update app_review
This commit is contained in:
@@ -1,8 +1,24 @@
|
||||
## 3.0.0
|
||||
|
||||
* **BREAKING**: `requestReview`, `writeReview`, and `storeListing` now return `Future<void>` instead of `Future<String?>`.
|
||||
* **BREAKING**: Errors are now thrown as `AppReviewException` instead of being returned as error strings.
|
||||
* **BREAKING**: Removed `url_launcher` dependency. Store opening logic is now native.
|
||||
* **BREAKING**: Removed `http` dependency. App ID lookup is now native.
|
||||
* **BREAKING**: Migrated all platform communication to **Pigeon** for type safety.
|
||||
* **FEAT**: Added `openStoreListing` and `openAppStoreReview` methods to API.
|
||||
* **FEAT**: Added `lookupAppId` method to API.
|
||||
* **FEAT**: (iOS/macOS) Added SwiftUI bridge for `requestReview` environment value (iOS 16+ / macOS 14+).
|
||||
* **FEAT**: (Android) Migrated to `com.google.android.play:review`.
|
||||
* **FEAT**: (Android) Added support for "Test Mode" (FakeReviewManager).
|
||||
* **CHORE**: Migrated iOS implementation to Swift.
|
||||
* **CHORE**: Removed `package_info_plus` dependency.
|
||||
|
||||
## 2.2.0
|
||||
* Update SDK constraints (Dart ^3.0.0, Flutter >=3.10.0)
|
||||
* Update dependencies (package_info_plus ^9.0.0, url_launcher ^6.3.0, http ^1.2.0)
|
||||
* Fix example app lint errors
|
||||
* Fix iOS Store Page and Write Review URLs (use apps.apple.com)
|
||||
* Fix Android build (add namespace, update SDK versions, migrate to v2 embedding)
|
||||
|
||||
## 2.1.2
|
||||
|
||||
|
||||
@@ -1,55 +1,100 @@
|
||||
[](https://github.com/fluttercommunity/community)
|
||||
# App Review
|
||||
|
||||
[](https://www.buymeacoffee.com/rodydavis)
|
||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WSH3GVC49GNNJ)
|
||||

|
||||
[](https://github.com/fluttercommunity/app_review)
|
||||
[](https://pub.dev/packages/app_review)
|
||||
A Flutter plugin for Requesting and Writing Reviews and Opening Store Listings for Android, iOS, and macOS.
|
||||
|
||||
# app_review
|
||||
This plugin allows you to prompt users to rate your app without leaving the application (In-App Review) or to deep-link users directly to your store listing or review page.
|
||||
|
||||

|
||||
---
|
||||
|
||||
Online Demo: https://fluttercommunity.github.io/app_review/
|
||||
## Operations
|
||||
|
||||
## Description
|
||||
Flutter Plugin for Requesting and Writing Reviews in Google Play and the App Store. Apps have to be published for the app to be found correctly.
|
||||
| Feature | Android | iOS | macOS | Windows | Linux | Web | Description |
|
||||
| :--- | :---: | :---: | :---: | :---: | :---: | :---: | :--- |
|
||||
| **Request Review** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | Native In-App Review prompt. |
|
||||
| **Open Store Listing** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | Opens the app's page on Play Store / App Store. |
|
||||
| **Open Write Review** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | Directs user to the "Write a Review" section. |
|
||||
| **Lookup App ID** | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | Fetches App ID/Track ID from iTunes Lookup API. |
|
||||
|
||||
## How To Use
|
||||
It's important to note that the App ID must match the App ID in Google Play and iTunes Connect. This can be changed in the Info.plist on iOS and app/build.gradle on Android. You will use this App ID for other services like Firebase, Admob and publishing the app.
|
||||
---
|
||||
|
||||
#### Android
|
||||
Opens In App Review but only if Play Services are installed on the device and the App is downloaded through the Play Store. Check out the [official documentation](https://developer.android.com/guide/playcore/in-app-review).
|
||||
|
||||
#### iOS
|
||||
iOS manages the pop-up requesting review within an app. You can call the code through `AppReview.requestReview` and if the user has "rate in apps" turned on, iOS will send "the request for the review" pop up. This is the required way for requesting reviews after iOS 10.3.
|
||||
|
||||
In debug mode it will always display. In apps through TestFlight, the `AppReview.requestReview` does nothing.
|
||||
|
||||
``` dart
|
||||
import 'dart:io';
|
||||
import 'package:app_review/app_review.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (Platform.isIOS) {
|
||||
AppReview.requestReview.then((onValue) {
|
||||
print(onValue);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What is onValue value?
|
||||
|
||||
Just a sanity check to make sure there were no errors.
|
||||
## Setup
|
||||
|
||||
### Android
|
||||
|
||||
You could store a timestamp to know when to call again.
|
||||
1. **Requirement**: Uses the [Google Play In-App Review API](https://developer.android.com/guide/playcore/in-app-review).
|
||||
2. **Usage**: Ensure your app is published on the Play Store for the real review flow to work.
|
||||
* **Test Mode**: You can enable `testMode: true` when calling `requestReview()` to use the `FakeReviewManager`, which simulates the review flow without requiring a published app.
|
||||
|
||||
### iOS
|
||||
|
||||
It doesn't really matter.
|
||||
1. **Requirement**: Uses `SKStoreReviewController`.
|
||||
2. **Usage**:
|
||||
* Development/TestFlight: The prompt may not always appear or may do nothing.
|
||||
* Production: Apple limits the prompt to display only **3 times per year**.
|
||||
* iOS 16+ Support: Uses `SwiftUI` environment value for requesting reviews if running in a SwiftUI context, otherwise falls back to `SKStoreReviewController`.
|
||||
|
||||
### macOS
|
||||
|
||||
1. **Requirement**: Uses `SKStoreReviewController` (macOS 10.14+) or `SwiftUI` requestReview (macOS 14+).
|
||||
2. **Sandboxing**: Ensure `com.apple.security.network.client` entitlement is active for API lookups if needed.
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Request In-App Review
|
||||
|
||||
Prompts the user to rate the app.
|
||||
|
||||
```dart
|
||||
import 'package:app_review/app_review.dart';
|
||||
|
||||
// ...
|
||||
|
||||
try {
|
||||
// Pass testMode: true to simulate review on Android (uses FakeReviewManager)
|
||||
await AppReview.requestReview(testMode: kDebugMode);
|
||||
} catch (e) {
|
||||
print('Request review failed: $e');
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Open Store Listing
|
||||
|
||||
Opens the App Store or Google Play Store page for the app.
|
||||
|
||||
```dart
|
||||
// Open for current app
|
||||
await AppReview.openStoreListing();
|
||||
|
||||
// OR open for a specific app ID/package name
|
||||
await AppReview.openStoreListing(storeId: "com.example.otherapp");
|
||||
```
|
||||
|
||||
### 3. Open Write Review Page
|
||||
|
||||
Attempts to open the store directly to the "Write a Review" screen.
|
||||
|
||||
```dart
|
||||
// For current app
|
||||
await AppReview.openAppStoreReview(); // iOS/macOS only
|
||||
// Android typically handles this via openStoreListing but we provide a unified API
|
||||
```
|
||||
|
||||
### 4. Lookup App ID (iOS/macOS)
|
||||
|
||||
Useful if you need to find your Apple App ID (Track ID) dynamically using your Bundle ID.
|
||||
|
||||
```dart
|
||||
String? appID = await AppReview.getIosAppId(
|
||||
bundleId: "com.example.app",
|
||||
countryCode: "US" // optional
|
||||
);
|
||||
print("App ID: $appID");
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example
|
||||
|
||||
Check out the `example` directory for a complete sample application.
|
||||
|
||||
@@ -22,10 +22,11 @@ rootProject.allprojects {
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
namespace 'com.appleeducate.appreview'
|
||||
compileSdkVersion 34
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
minSdkVersion 21
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
lintOptions {
|
||||
@@ -35,5 +36,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
api 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.google.android.play:core:1.10.3'
|
||||
implementation 'com.google.android.play:review:2.0.1'
|
||||
}
|
||||
|
||||
+113
-63
@@ -1,6 +1,8 @@
|
||||
package com.appleeducate.appreview;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -8,128 +10,175 @@ import androidx.annotation.Nullable;
|
||||
import com.google.android.play.core.review.ReviewInfo;
|
||||
import com.google.android.play.core.review.ReviewManager;
|
||||
import com.google.android.play.core.review.ReviewManagerFactory;
|
||||
import com.google.android.play.core.tasks.OnCompleteListener;
|
||||
import com.google.android.play.core.tasks.Task;
|
||||
import com.google.android.play.core.review.testing.FakeReviewManager;
|
||||
import com.google.android.gms.tasks.OnCompleteListener;
|
||||
import com.google.android.gms.tasks.Task;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import io.flutter.embedding.engine.plugins.FlutterPlugin;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
|
||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.plugin.common.PluginRegistry.Registrar;
|
||||
|
||||
/** AppReviewPlugin */
|
||||
public class AppReviewPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware {
|
||||
private static final String CHANNEL_NAME = "app_review";
|
||||
|
||||
public class AppReviewPlugin implements FlutterPlugin, Messages.AppReviewApi, ActivityAware {
|
||||
private WeakReference<Activity> currentActivity;
|
||||
|
||||
private MethodChannel channel;
|
||||
|
||||
private ReviewManager manager;
|
||||
@Nullable
|
||||
private ReviewInfo reviewInfo;
|
||||
|
||||
public AppReviewPlugin() {
|
||||
}
|
||||
|
||||
/** Plugin registration. */
|
||||
public static void registerWith(Registrar registrar) {
|
||||
AppReviewPlugin simplePermissionsPlugin = new AppReviewPlugin();
|
||||
simplePermissionsPlugin.setupChannel(registrar.messenger());
|
||||
// registrar.addRequestPermissionsResultListener(simplePermissionsPlugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
|
||||
setupChannel(binding.getBinaryMessenger());
|
||||
Messages.AppReviewApi.setUp(binding.getBinaryMessenger(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
|
||||
teardownChannel();
|
||||
}
|
||||
|
||||
private void setupChannel(BinaryMessenger messenger) {
|
||||
channel = new MethodChannel(messenger, CHANNEL_NAME);
|
||||
channel.setMethodCallHandler(this);
|
||||
}
|
||||
|
||||
private void teardownChannel() {
|
||||
channel.setMethodCallHandler(null);
|
||||
channel = null;
|
||||
Messages.AppReviewApi.setUp(binding.getBinaryMessenger(), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, @NonNull Result result) {
|
||||
String method = call.method;
|
||||
switch (method) {
|
||||
case "isRequestReviewAvailable":
|
||||
isRequestReviewAvailable(result);
|
||||
break;
|
||||
case "requestReview":
|
||||
requestReview(result);
|
||||
break;
|
||||
default:
|
||||
result.notImplemented();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void isRequestReviewAvailable(final Result result) {
|
||||
if (currentActivity == null || currentActivity.get() == null) {
|
||||
result.error("error", "Android activity not available", null);
|
||||
public void isRequestReviewAvailable(Messages.Result<Boolean> result) {
|
||||
if (manager == null) {
|
||||
result.error(new Exception("ReviewManager not initialized"));
|
||||
return;
|
||||
}
|
||||
ReviewManager manager = ReviewManagerFactory.create(currentActivity.get());
|
||||
Task<ReviewInfo> request = manager.requestReviewFlow();
|
||||
request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<ReviewInfo> task) {
|
||||
if (task.isSuccessful()) {
|
||||
reviewInfo = task.getResult();
|
||||
result.success("1");
|
||||
result.success(true);
|
||||
} else {
|
||||
result.success("0");
|
||||
result.success(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void requestReview(final Result result) {
|
||||
@Override
|
||||
public void openStoreListing(@Nullable String storeId, Messages.VoidResult result) {
|
||||
if (currentActivity == null || currentActivity.get() == null) {
|
||||
result.error("error", "Android activity not available", null);
|
||||
result.error(new Exception("Android activity not available"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + storeId));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
currentActivity.get().startActivity(intent);
|
||||
result.success();
|
||||
} catch (android.content.ActivityNotFoundException anfe) {
|
||||
try {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + storeId));
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
currentActivity.get().startActivity(intent);
|
||||
result.success();
|
||||
} catch (Exception e) {
|
||||
result.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openAppStoreReview(@Nullable String storeId, Messages.VoidResult result) {
|
||||
openStoreListing(storeId, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lookupAppId(@NonNull String bundleId, @Nullable String countryCode, Messages.NullableResult<String> result) {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String country = countryCode != null ? countryCode : "";
|
||||
java.net.URL url = new java.net.URL("https://itunes.apple.com/" + country + "/lookup?bundleId=" + bundleId);
|
||||
java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
|
||||
if (connection.getResponseCode() == 200) {
|
||||
java.io.InputStream inputStream = connection.getInputStream();
|
||||
java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
|
||||
StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
reader.close();
|
||||
|
||||
org.json.JSONObject json = new org.json.JSONObject(response.toString());
|
||||
org.json.JSONArray results = json.getJSONArray("results");
|
||||
if (results.length() > 0) {
|
||||
org.json.JSONObject firstResult = results.getJSONObject(0);
|
||||
if (firstResult.has("trackId")) {
|
||||
String trackId = String.valueOf(firstResult.getInt("trackId"));
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> result.success(trackId));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> result.success(null));
|
||||
} catch (Exception e) {
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> result.error(e));
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getBundleId(Messages.Result<String> result) {
|
||||
if (currentActivity == null || currentActivity.get() == null) {
|
||||
result.error(new Exception("Android activity not available"));
|
||||
return;
|
||||
}
|
||||
result.success(currentActivity.get().getPackageName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestReview(@NonNull Boolean testMode, Messages.NullableResult<String> result) {
|
||||
if (currentActivity == null || currentActivity.get() == null) {
|
||||
result.error(new Exception("Android activity not available"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (testMode) {
|
||||
manager = new FakeReviewManager(currentActivity.get());
|
||||
} else {
|
||||
if (manager == null || manager instanceof FakeReviewManager) {
|
||||
manager = ReviewManagerFactory.create(currentActivity.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (reviewInfo == null) {
|
||||
getReviewInfoAndRequestReview(result);
|
||||
getReviewInfoAndRequestReview(testMode, result);
|
||||
return;
|
||||
}
|
||||
ReviewManager manager = ReviewManagerFactory.create(currentActivity.get());
|
||||
Task<Void> task = manager.launchReviewFlow(currentActivity.get(), reviewInfo);
|
||||
task.addOnCompleteListener(new OnCompleteListener<Void>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<Void> task) {
|
||||
reviewInfo = null;
|
||||
result.success("Success: " + task.isSuccessful());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void getReviewInfoAndRequestReview(final Result result) {
|
||||
ReviewManager manager = ReviewManagerFactory.create(currentActivity.get());
|
||||
private void getReviewInfoAndRequestReview(Boolean testMode, final Messages.NullableResult<String> result) {
|
||||
if (manager == null) {
|
||||
result.error(new Exception("ReviewManager not initialized"));
|
||||
return;
|
||||
}
|
||||
Task<ReviewInfo> request = manager.requestReviewFlow();
|
||||
request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>() {
|
||||
@Override
|
||||
public void onComplete(@NonNull Task<ReviewInfo> task) {
|
||||
if (task.isSuccessful()) {
|
||||
reviewInfo = task.getResult();
|
||||
requestReview(result);
|
||||
requestReview(testMode, result);
|
||||
} else {
|
||||
result.error("Requesting review not possible", null, null);
|
||||
result.error(new Exception("Requesting review not possible"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -138,6 +187,7 @@ public class AppReviewPlugin implements FlutterPlugin, MethodCallHandler, Activi
|
||||
@Override
|
||||
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
|
||||
currentActivity = new WeakReference<>(binding.getActivity());
|
||||
manager = ReviewManagerFactory.create(binding.getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,6 +203,6 @@ public class AppReviewPlugin implements FlutterPlugin, MethodCallHandler, Activi
|
||||
@Override
|
||||
public void onDetachedFromActivity() {
|
||||
currentActivity = null;
|
||||
manager = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
|
||||
|
||||
package com.appleeducate.appreview
|
||||
|
||||
import android.util.Log
|
||||
import io.flutter.plugin.common.BasicMessageChannel
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MessageCodec
|
||||
import io.flutter.plugin.common.StandardMethodCodec
|
||||
import io.flutter.plugin.common.StandardMessageCodec
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
private object MessagesPigeonUtils {
|
||||
|
||||
fun wrapResult(result: Any?): List<Any?> {
|
||||
return listOf(result)
|
||||
}
|
||||
|
||||
fun wrapError(exception: Throwable): List<Any?> {
|
||||
return if (exception is FlutterError) {
|
||||
listOf(
|
||||
exception.code,
|
||||
exception.message,
|
||||
exception.details
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
exception.javaClass.simpleName,
|
||||
exception.toString(),
|
||||
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error class for passing custom error details to Flutter via a thrown PlatformException.
|
||||
* @property code The error code.
|
||||
* @property message The error message.
|
||||
* @property details The error details. Must be a datatype supported by the api codec.
|
||||
*/
|
||||
class FlutterError (
|
||||
val code: String,
|
||||
override val message: String? = null,
|
||||
val details: Any? = null
|
||||
) : Throwable()
|
||||
private open class MessagesPigeonCodec : StandardMessageCodec() {
|
||||
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
|
||||
return super.readValueOfType(type, buffer)
|
||||
}
|
||||
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
|
||||
super.writeValue(stream, value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
interface AppReviewApi {
|
||||
/**
|
||||
* Request review.
|
||||
*
|
||||
* Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
* Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
*
|
||||
* Returns string with details message.
|
||||
*/
|
||||
fun requestReview(testMode: Boolean, callback: (Result<String?>) -> Unit)
|
||||
/** Check if [requestReview] feature available. */
|
||||
fun isRequestReviewAvailable(callback: (Result<Boolean>) -> Unit)
|
||||
/**
|
||||
* Opens the store listing for the specified app.
|
||||
*
|
||||
* [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
*/
|
||||
fun openStoreListing(storeId: String?, callback: (Result<Unit>) -> Unit)
|
||||
/**
|
||||
* Opens the App Store review page (iOS/macOS only).
|
||||
*
|
||||
* [storeId] is the App ID.
|
||||
*/
|
||||
fun openAppStoreReview(storeId: String?, callback: (Result<Unit>) -> Unit)
|
||||
/** Returns package name for application. */
|
||||
fun getBundleId(callback: (Result<String>) -> Unit)
|
||||
/**
|
||||
* Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
*
|
||||
* [bundleId] is the bundle identifier to look up.
|
||||
* [countryCode] is the optional country code.
|
||||
*/
|
||||
fun lookupAppId(bundleId: String, countryCode: String?, callback: (Result<String?>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by AppReviewApi. */
|
||||
val codec: MessageCodec<Any?> by lazy {
|
||||
MessagesPigeonCodec()
|
||||
}
|
||||
/** Sets up an instance of `AppReviewApi` to handle messages through the `binaryMessenger`. */
|
||||
@JvmOverloads
|
||||
fun setUp(binaryMessenger: BinaryMessenger, api: AppReviewApi?, messageChannelSuffix: String = "") {
|
||||
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.requestReview$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val testModeArg = args[0] as Boolean
|
||||
api.requestReview(testModeArg) { result: Result<String?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.isRequestReviewAvailable$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.isRequestReviewAvailable{ result: Result<Boolean> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.openStoreListing$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val storeIdArg = args[0] as String?
|
||||
api.openStoreListing(storeIdArg) { result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.openAppStoreReview$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val storeIdArg = args[0] as String?
|
||||
api.openAppStoreReview(storeIdArg) { result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.getBundleId$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { _, reply ->
|
||||
api.getBundleId{ result: Result<String> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.lookupAppId$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val bundleIdArg = args[0] as String
|
||||
val countryCodeArg = args[1] as String?
|
||||
api.lookupAppId(bundleIdArg, countryCodeArg) { result: Result<String?> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(MessagesPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
val data = result.getOrNull()
|
||||
reply.reply(MessagesPigeonUtils.wrapResult(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
package com.appleeducate.appreview;
|
||||
|
||||
import android.util.Log;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import io.flutter.plugin.common.BasicMessageChannel;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugin.common.MessageCodec;
|
||||
import io.flutter.plugin.common.StandardMessageCodec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Generated class from Pigeon. */
|
||||
@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression", "serial"})
|
||||
public class Messages {
|
||||
|
||||
/** Error class for passing custom error details to Flutter via a thrown PlatformException. */
|
||||
public static class FlutterError extends RuntimeException {
|
||||
|
||||
/** The error code. */
|
||||
public final String code;
|
||||
|
||||
/** The error details. Must be a datatype supported by the api codec. */
|
||||
public final Object details;
|
||||
|
||||
public FlutterError(@NonNull String code, @Nullable String message, @Nullable Object details)
|
||||
{
|
||||
super(message);
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected static ArrayList<Object> wrapError(@NonNull Throwable exception) {
|
||||
ArrayList<Object> errorList = new ArrayList<>(3);
|
||||
if (exception instanceof FlutterError) {
|
||||
FlutterError error = (FlutterError) exception;
|
||||
errorList.add(error.code);
|
||||
errorList.add(error.getMessage());
|
||||
errorList.add(error.details);
|
||||
} else {
|
||||
errorList.add(exception.toString());
|
||||
errorList.add(exception.getClass().getSimpleName());
|
||||
errorList.add(
|
||||
"Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
|
||||
}
|
||||
return errorList;
|
||||
}
|
||||
|
||||
private static class PigeonCodec extends StandardMessageCodec {
|
||||
public static final PigeonCodec INSTANCE = new PigeonCodec();
|
||||
|
||||
private PigeonCodec() {}
|
||||
|
||||
@Override
|
||||
protected Object readValueOfType(byte type, @NonNull ByteBuffer buffer) {
|
||||
switch (type) {
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeValue(@NonNull ByteArrayOutputStream stream, Object value) {
|
||||
{
|
||||
super.writeValue(stream, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Asynchronous error handling return type for non-nullable API method returns. */
|
||||
public interface Result<T> {
|
||||
/** Success case callback method for handling returns. */
|
||||
void success(@NonNull T result);
|
||||
|
||||
/** Failure case callback method for handling errors. */
|
||||
void error(@NonNull Throwable error);
|
||||
}
|
||||
/** Asynchronous error handling return type for nullable API method returns. */
|
||||
public interface NullableResult<T> {
|
||||
/** Success case callback method for handling returns. */
|
||||
void success(@Nullable T result);
|
||||
|
||||
/** Failure case callback method for handling errors. */
|
||||
void error(@NonNull Throwable error);
|
||||
}
|
||||
/** Asynchronous error handling return type for void API method returns. */
|
||||
public interface VoidResult {
|
||||
/** Success case callback method for handling returns. */
|
||||
void success();
|
||||
|
||||
/** Failure case callback method for handling errors. */
|
||||
void error(@NonNull Throwable error);
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
public interface AppReviewApi {
|
||||
/**
|
||||
* Request review.
|
||||
*
|
||||
* Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
* Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
*
|
||||
* Returns string with details message.
|
||||
*/
|
||||
void requestReview(@NonNull Boolean testMode, @NonNull NullableResult<String> result);
|
||||
/** Check if [requestReview] feature available. */
|
||||
void isRequestReviewAvailable(@NonNull Result<Boolean> result);
|
||||
/**
|
||||
* Opens the store listing for the specified app.
|
||||
*
|
||||
* [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
*/
|
||||
void openStoreListing(@Nullable String storeId, @NonNull VoidResult result);
|
||||
/**
|
||||
* Opens the App Store review page (iOS/macOS only).
|
||||
*
|
||||
* [storeId] is the App ID.
|
||||
*/
|
||||
void openAppStoreReview(@Nullable String storeId, @NonNull VoidResult result);
|
||||
/** Returns package name for application. */
|
||||
void getBundleId(@NonNull Result<String> result);
|
||||
/**
|
||||
* Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
*
|
||||
* [bundleId] is the bundle identifier to look up.
|
||||
* [countryCode] is the optional country code.
|
||||
*/
|
||||
void lookupAppId(@NonNull String bundleId, @Nullable String countryCode, @NonNull NullableResult<String> result);
|
||||
|
||||
/** The codec used by AppReviewApi. */
|
||||
static @NonNull MessageCodec<Object> getCodec() {
|
||||
return PigeonCodec.INSTANCE;
|
||||
}
|
||||
/**Sets up an instance of `AppReviewApi` to handle messages through the `binaryMessenger`. */
|
||||
static void setUp(@NonNull BinaryMessenger binaryMessenger, @Nullable AppReviewApi api) {
|
||||
setUp(binaryMessenger, "", api);
|
||||
}
|
||||
static void setUp(@NonNull BinaryMessenger binaryMessenger, @NonNull String messageChannelSuffix, @Nullable AppReviewApi api) {
|
||||
messageChannelSuffix = messageChannelSuffix.isEmpty() ? "" : "." + messageChannelSuffix;
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.requestReview" + messageChannelSuffix, getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<>();
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
Boolean testModeArg = (Boolean) args.get(0);
|
||||
NullableResult<String> resultCallback =
|
||||
new NullableResult<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.add(0, result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.requestReview(testModeArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.isRequestReviewAvailable" + messageChannelSuffix, getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<>();
|
||||
Result<Boolean> resultCallback =
|
||||
new Result<Boolean>() {
|
||||
public void success(Boolean result) {
|
||||
wrapped.add(0, result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.isRequestReviewAvailable(resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.openStoreListing" + messageChannelSuffix, getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<>();
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
String storeIdArg = (String) args.get(0);
|
||||
VoidResult resultCallback =
|
||||
new VoidResult() {
|
||||
public void success() {
|
||||
wrapped.add(0, null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.openStoreListing(storeIdArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.openAppStoreReview" + messageChannelSuffix, getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<>();
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
String storeIdArg = (String) args.get(0);
|
||||
VoidResult resultCallback =
|
||||
new VoidResult() {
|
||||
public void success() {
|
||||
wrapped.add(0, null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.openAppStoreReview(storeIdArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.getBundleId" + messageChannelSuffix, getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<>();
|
||||
Result<String> resultCallback =
|
||||
new Result<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.add(0, result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.getBundleId(resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.app_review.AppReviewApi.lookupAppId" + messageChannelSuffix, getCodec());
|
||||
if (api != null) {
|
||||
channel.setMessageHandler(
|
||||
(message, reply) -> {
|
||||
ArrayList<Object> wrapped = new ArrayList<>();
|
||||
ArrayList<Object> args = (ArrayList<Object>) message;
|
||||
String bundleIdArg = (String) args.get(0);
|
||||
String countryCodeArg = (String) args.get(1);
|
||||
NullableResult<String> resultCallback =
|
||||
new NullableResult<String>() {
|
||||
public void success(String result) {
|
||||
wrapped.add(0, result);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.lookupAppId(bundleIdArg, countryCodeArg, resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,32 +2,20 @@ PODS:
|
||||
- app_review (1.0.1):
|
||||
- Flutter
|
||||
- Flutter (1.0.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_review (from `.symlinks/plugins/app_review/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_review:
|
||||
:path: ".symlinks/plugins/app_review/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_review: e2799b306b6460ca7e5f4df53f4f1a1473456ef0
|
||||
app_review: f75e3250ef5694e328a5ecb3f4c445bc7b32d821
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_review/app_review.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -38,7 +40,7 @@ class _HomePageState extends State<HomePage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
AppReview.getAppID.then((onValue) {
|
||||
AppReview.getAppId().then((onValue) {
|
||||
setState(() {
|
||||
appID = onValue ?? '';
|
||||
});
|
||||
@@ -48,13 +50,20 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
String appID = "";
|
||||
final List<String> _logs = [];
|
||||
bool _useAndroidTestMode = true;
|
||||
|
||||
final testAppId = () {
|
||||
if (Platform.isAndroid) return 'com.google.android.googlequicksearchbox';
|
||||
if (Platform.isIOS || Platform.isMacOS) return '640199958';
|
||||
return null;
|
||||
}();
|
||||
|
||||
void log(String? message) {
|
||||
if (message != null) {
|
||||
debugPrint(message);
|
||||
setState(() {
|
||||
_logs.insert(0, message);
|
||||
});
|
||||
debugPrint(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,13 +82,24 @@ class _HomePageState extends State<HomePage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (Platform.isAndroid)
|
||||
SwitchListTile(
|
||||
title: const Text('Use Android Test Mode'),
|
||||
subtitle: const Text('Uses FakeReviewManager'),
|
||||
value: _useAndroidTestMode,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_useAndroidTestMode = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
_buildInfoCard(
|
||||
context,
|
||||
title: 'App ID',
|
||||
subtitle: appID.isEmpty ? 'Loading...' : appID,
|
||||
icon: Icons.info_outline,
|
||||
onTap: () {
|
||||
AppReview.getAppID.then(log);
|
||||
AppReview.getAppId().then(log);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -98,24 +118,16 @@ class _HomePageState extends State<HomePage> {
|
||||
icon: Icons.storefront,
|
||||
color: Colors.orange,
|
||||
onTap: () async {
|
||||
final id = await AppReview.getIosAppId();
|
||||
if (id == null || id.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'App not found in store. Using Apple Developer App for testing.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Use Apple Developer App ID for testing
|
||||
// https://apps.apple.com/us/app/apple-developer/id640199958
|
||||
AppReview.openAppStore(
|
||||
fallbackUrl:
|
||||
'https://apps.apple.com/us/app/apple-developer/id640199958',
|
||||
).then(log);
|
||||
} else {
|
||||
AppReview.storeListing.then(log);
|
||||
try {
|
||||
await AppReview.storeListing(
|
||||
appStoreId: (Platform.isIOS || Platform.isMacOS)
|
||||
? testAppId
|
||||
: null,
|
||||
playStoreId: Platform.isAndroid ? testAppId : null,
|
||||
);
|
||||
log('Opened Store Listing');
|
||||
} catch (e) {
|
||||
log('Failed to open store listing: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -126,8 +138,15 @@ class _HomePageState extends State<HomePage> {
|
||||
subtitle: 'Ask the user to rate the app',
|
||||
icon: Icons.star_rate_rounded,
|
||||
color: Colors.amber,
|
||||
onTap: () {
|
||||
AppReview.requestReview.then(log);
|
||||
onTap: () async {
|
||||
try {
|
||||
await AppReview.requestReview(
|
||||
useAndroidTestMode: _useAndroidTestMode,
|
||||
);
|
||||
log('Requested Review');
|
||||
} catch (e) {
|
||||
log('Request Review failed: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
@@ -138,23 +157,17 @@ class _HomePageState extends State<HomePage> {
|
||||
icon: Icons.rate_review_outlined,
|
||||
color: Colors.green,
|
||||
onTap: () async {
|
||||
final id = await AppReview.getIosAppId();
|
||||
if (id == null || id.isEmpty) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'App not found in store. Using Apple Developer App for testing.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Use Apple Developer App ID for testing
|
||||
AppReview.openIosReview(
|
||||
appId: '640199958',
|
||||
compose: true,
|
||||
).then(log);
|
||||
} else {
|
||||
AppReview.writeReview.then(log);
|
||||
try {
|
||||
await AppReview.writeReview(
|
||||
appStoreId: (Platform.isIOS || Platform.isMacOS)
|
||||
? testAppId
|
||||
: null,
|
||||
playStoreId: Platform.isAndroid ? testAppId : null,
|
||||
useAndroidTestMode: _useAndroidTestMode,
|
||||
);
|
||||
log('Opened Write Review');
|
||||
} catch (e) {
|
||||
log('Write Review failed: $e');
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
flutter/ephemeral
|
||||
@@ -1,128 +0,0 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "app_review_example")
|
||||
# The unique GTK application identifier for this application. See:
|
||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||
set(APPLICATION_ID "com.example.app_review_example")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(SET CMP0063 NEW)
|
||||
|
||||
# Load bundled libraries from the lib/ directory relative to the binary.
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
|
||||
|
||||
# Root filesystem for cross-building.
|
||||
if(FLUTTER_TARGET_PLATFORM_SYSROOT)
|
||||
set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
|
||||
endif()
|
||||
|
||||
# Define build configuration options.
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_14)
|
||||
target_compile_options(${TARGET} PRIVATE -Wall -Werror)
|
||||
target_compile_options(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:-O3>")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<NOT:$<CONFIG:Debug>>:NDEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
|
||||
# Only the install-generated bundle's copy of the executable will launch
|
||||
# correctly, since the resources must in the right relative locations. To avoid
|
||||
# people trying to run the unbundled copy, put it in a subdirectory instead of
|
||||
# the default top-level location.
|
||||
set_target_properties(${BINARY_NAME}
|
||||
PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
|
||||
)
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# By default, "installing" just makes a relocatable bundle in the build
|
||||
# directory.
|
||||
set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
# Start with a clean build bundle directory every time.
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
|
||||
" COMPONENT Runtime)
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
|
||||
install(FILES "${bundled_library}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endforeach(bundled_library)
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
@@ -1,88 +0,0 @@
|
||||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
|
||||
# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
|
||||
# which isn't available in 3.10.
|
||||
function(list_prepend LIST_NAME PREFIX)
|
||||
set(NEW_LIST "")
|
||||
foreach(element ${${LIST_NAME}})
|
||||
list(APPEND NEW_LIST "${PREFIX}${element}")
|
||||
endforeach(element)
|
||||
set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# === Flutter Library ===
|
||||
# System-level dependencies.
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
|
||||
pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"fl_basic_message_channel.h"
|
||||
"fl_binary_codec.h"
|
||||
"fl_binary_messenger.h"
|
||||
"fl_dart_project.h"
|
||||
"fl_engine.h"
|
||||
"fl_json_message_codec.h"
|
||||
"fl_json_method_codec.h"
|
||||
"fl_message_codec.h"
|
||||
"fl_method_call.h"
|
||||
"fl_method_channel.h"
|
||||
"fl_method_codec.h"
|
||||
"fl_method_response.h"
|
||||
"fl_plugin_registrar.h"
|
||||
"fl_plugin_registry.h"
|
||||
"fl_standard_message_codec.h"
|
||||
"fl_standard_method_codec.h"
|
||||
"fl_string_codec.h"
|
||||
"fl_value.h"
|
||||
"fl_view.h"
|
||||
"flutter_linux.h"
|
||||
)
|
||||
list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
|
||||
target_link_libraries(flutter INTERFACE
|
||||
PkgConfig::GTK
|
||||
PkgConfig::GLIB
|
||||
PkgConfig::GIO
|
||||
)
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/_phony_
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
|
||||
${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
)
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void fl_register_plugins(FlPluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -1,24 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -1,26 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME}
|
||||
"main.cc"
|
||||
"my_application.cc"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the application ID.
|
||||
add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
|
||||
|
||||
# Add dependency libraries. Add any application-specific dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
|
||||
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
@@ -1,6 +0,0 @@
|
||||
#include "my_application.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
g_autoptr(MyApplication) app = my_application_new();
|
||||
return g_application_run(G_APPLICATION(app), argc, argv);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
#include "my_application.h"
|
||||
|
||||
#include <flutter_linux/flutter_linux.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
struct _MyApplication {
|
||||
GtkApplication parent_instance;
|
||||
char** dart_entrypoint_arguments;
|
||||
};
|
||||
|
||||
G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
||||
|
||||
// Called when first Flutter frame received.
|
||||
static void first_frame_cb(MyApplication* self, FlView* view) {
|
||||
gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view)));
|
||||
}
|
||||
|
||||
// Implements GApplication::activate.
|
||||
static void my_application_activate(GApplication* application) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
GtkWindow* window =
|
||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||
|
||||
// Use a header bar when running in GNOME as this is the common style used
|
||||
// by applications and is the setup most users will be using (e.g. Ubuntu
|
||||
// desktop).
|
||||
// If running on X and not using GNOME then just use a traditional title bar
|
||||
// in case the window manager does more exotic layout, e.g. tiling.
|
||||
// If running on Wayland assume the header bar will work (may need changing
|
||||
// if future cases occur).
|
||||
gboolean use_header_bar = TRUE;
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
GdkScreen* screen = gtk_window_get_screen(window);
|
||||
if (GDK_IS_X11_SCREEN(screen)) {
|
||||
const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
|
||||
if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
|
||||
use_header_bar = FALSE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (use_header_bar) {
|
||||
GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
|
||||
gtk_widget_show(GTK_WIDGET(header_bar));
|
||||
gtk_header_bar_set_title(header_bar, "app_review_example");
|
||||
gtk_header_bar_set_show_close_button(header_bar, TRUE);
|
||||
gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
|
||||
} else {
|
||||
gtk_window_set_title(window, "app_review_example");
|
||||
}
|
||||
|
||||
gtk_window_set_default_size(window, 1280, 720);
|
||||
|
||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||
fl_dart_project_set_dart_entrypoint_arguments(
|
||||
project, self->dart_entrypoint_arguments);
|
||||
|
||||
FlView* view = fl_view_new(project);
|
||||
GdkRGBA background_color;
|
||||
// Background defaults to black, override it here if necessary, e.g. #00000000
|
||||
// for transparent.
|
||||
gdk_rgba_parse(&background_color, "#000000");
|
||||
fl_view_set_background_color(view, &background_color);
|
||||
gtk_widget_show(GTK_WIDGET(view));
|
||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||
|
||||
// Show the window when Flutter renders.
|
||||
// Requires the view to be realized so we can start rendering.
|
||||
g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb),
|
||||
self);
|
||||
gtk_widget_realize(GTK_WIDGET(view));
|
||||
|
||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||
|
||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||
}
|
||||
|
||||
// Implements GApplication::local_command_line.
|
||||
static gboolean my_application_local_command_line(GApplication* application,
|
||||
gchar*** arguments,
|
||||
int* exit_status) {
|
||||
MyApplication* self = MY_APPLICATION(application);
|
||||
// Strip out the first argument as it is the binary name.
|
||||
self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
|
||||
|
||||
g_autoptr(GError) error = nullptr;
|
||||
if (!g_application_register(application, nullptr, &error)) {
|
||||
g_warning("Failed to register: %s", error->message);
|
||||
*exit_status = 1;
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
g_application_activate(application);
|
||||
*exit_status = 0;
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Implements GApplication::startup.
|
||||
static void my_application_startup(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application startup.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->startup(application);
|
||||
}
|
||||
|
||||
// Implements GApplication::shutdown.
|
||||
static void my_application_shutdown(GApplication* application) {
|
||||
// MyApplication* self = MY_APPLICATION(object);
|
||||
|
||||
// Perform any actions required at application shutdown.
|
||||
|
||||
G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application);
|
||||
}
|
||||
|
||||
// Implements GObject::dispose.
|
||||
static void my_application_dispose(GObject* object) {
|
||||
MyApplication* self = MY_APPLICATION(object);
|
||||
g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
|
||||
G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
|
||||
}
|
||||
|
||||
static void my_application_class_init(MyApplicationClass* klass) {
|
||||
G_APPLICATION_CLASS(klass)->activate = my_application_activate;
|
||||
G_APPLICATION_CLASS(klass)->local_command_line =
|
||||
my_application_local_command_line;
|
||||
G_APPLICATION_CLASS(klass)->startup = my_application_startup;
|
||||
G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown;
|
||||
G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
|
||||
}
|
||||
|
||||
static void my_application_init(MyApplication* self) {}
|
||||
|
||||
MyApplication* my_application_new() {
|
||||
// Set the program name to the application ID, which helps various systems
|
||||
// like GTK and desktop environments map this running application to its
|
||||
// corresponding .desktop file. This ensures better integration by allowing
|
||||
// the application to be recognized beyond its binary name.
|
||||
g_set_prgname(APPLICATION_ID);
|
||||
|
||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||
"application-id", APPLICATION_ID, "flags",
|
||||
G_APPLICATION_NON_UNIQUE, nullptr));
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#ifndef FLUTTER_MY_APPLICATION_H_
|
||||
#define FLUTTER_MY_APPLICATION_H_
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
G_DECLARE_FINAL_TYPE(MyApplication,
|
||||
my_application,
|
||||
MY,
|
||||
APPLICATION,
|
||||
GtkApplication)
|
||||
|
||||
/**
|
||||
* my_application_new:
|
||||
*
|
||||
* Creates a new Flutter-based application.
|
||||
*
|
||||
* Returns: a new #MyApplication.
|
||||
*/
|
||||
MyApplication* my_application_new();
|
||||
|
||||
#endif // FLUTTER_MY_APPLICATION_H_
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>logFormatVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>logs</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastAccessedDate</key>
|
||||
<date>2026-01-15T08:24:24Z</date>
|
||||
<key>WorkspacePath</key>
|
||||
<string>/Users/rodydavis/Developer/packages.dart/packages/app_review/example/macos/Runner.xcworkspace</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -5,10 +5,8 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import package_info_plus
|
||||
import url_launcher_macos
|
||||
import app_review
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
AppReviewPlugin.register(with: registry.registrar(forPlugin: "AppReviewPlugin"))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
PODS:
|
||||
- app_review (0.0.1):
|
||||
- FlutterMacOS
|
||||
- FlutterMacOS (1.0.0)
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_review (from `Flutter/ephemeral/.symlinks/plugins/app_review/macos`)
|
||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
app_review:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/app_review/macos
|
||||
FlutterMacOS:
|
||||
:path: Flutter/ephemeral
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_review: e5e3799d29ec0a15e992f8f5074243fb7f607542
|
||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||
|
||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -27,6 +27,8 @@
|
||||
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
|
||||
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
|
||||
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
|
||||
523253863C87D949D5323A92 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FFA6F68B202CAAEDA3961C4 /* Pods_Runner.framework */; };
|
||||
BC7CA3CD3D8B3B67AD4C6654 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5398DE9E41BAF669BBB5D39 /* Pods_RunnerTests.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -60,11 +62,14 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0A0751204CD8C4AAE8F48B4F /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
0D8B601B159A85C06A4FF6D6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
3327A7CB862D93EDD8141A22 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; };
|
||||
335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; };
|
||||
33CC10ED2044A3C60003C045 /* app_review_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "app_review_example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10ED2044A3C60003C045 /* app_review_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app_review_example.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; };
|
||||
33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
@@ -76,8 +81,13 @@
|
||||
33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
|
||||
33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
|
||||
33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
|
||||
78CEC52B80A5FEAD79EF8817 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
|
||||
93AEDA44C58789CF3DD33D09 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9FFA6F68B202CAAEDA3961C4 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
DA30C72050056A238B9C24B8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
F5398DE9E41BAF669BBB5D39 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -85,6 +95,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
BC7CA3CD3D8B3B67AD4C6654 /* Pods_RunnerTests.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -92,6 +103,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
523253863C87D949D5323A92 /* Pods_Runner.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -125,6 +137,7 @@
|
||||
331C80D6294CF71000263BE5 /* RunnerTests */,
|
||||
33CC10EE2044A3C60003C045 /* Products */,
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */,
|
||||
61318E44D8FD81ED5CACDF94 /* Pods */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -172,9 +185,25 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
61318E44D8FD81ED5CACDF94 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3327A7CB862D93EDD8141A22 /* Pods-Runner.debug.xcconfig */,
|
||||
DA30C72050056A238B9C24B8 /* Pods-Runner.release.xcconfig */,
|
||||
78CEC52B80A5FEAD79EF8817 /* Pods-Runner.profile.xcconfig */,
|
||||
0A0751204CD8C4AAE8F48B4F /* Pods-RunnerTests.debug.xcconfig */,
|
||||
93AEDA44C58789CF3DD33D09 /* Pods-RunnerTests.release.xcconfig */,
|
||||
0D8B601B159A85C06A4FF6D6 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73912EC22F37F3D000D13A0 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9FFA6F68B202CAAEDA3961C4 /* Pods_Runner.framework */,
|
||||
F5398DE9E41BAF669BBB5D39 /* Pods_RunnerTests.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -186,6 +215,7 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
A7DF2AA218C96A56E9BA22ED /* [CP] Check Pods Manifest.lock */,
|
||||
331C80D1294CF70F00263BE5 /* Sources */,
|
||||
331C80D2294CF70F00263BE5 /* Frameworks */,
|
||||
331C80D3294CF70F00263BE5 /* Resources */,
|
||||
@@ -204,11 +234,13 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
435C51595CFCA62A73DC3148 /* [CP] Check Pods Manifest.lock */,
|
||||
33CC10E92044A3C60003C045 /* Sources */,
|
||||
33CC10EA2044A3C60003C045 /* Frameworks */,
|
||||
33CC10EB2044A3C60003C045 /* Resources */,
|
||||
33CC110E2044A8840003C045 /* Bundle Framework */,
|
||||
3399D490228B24CF009A79C7 /* ShellScript */,
|
||||
A82CCE2AC520EA8663BEF62F /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -329,6 +361,67 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
|
||||
};
|
||||
435C51595CFCA62A73DC3148 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A7DF2AA218C96A56E9BA22ED /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
A82CCE2AC520EA8663BEF62F /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -380,6 +473,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
331C80DB294CF71000263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 0A0751204CD8C4AAE8F48B4F /* Pods-RunnerTests.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -394,6 +488,7 @@
|
||||
};
|
||||
331C80DC294CF71000263BE5 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 93AEDA44C58789CF3DD33D09 /* Pods-RunnerTests.release.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
@@ -408,6 +503,7 @@
|
||||
};
|
||||
331C80DD294CF71000263BE5 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 0D8B601B159A85C06A4FF6D6 /* Pods-RunnerTests.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
|
||||
+3
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 917 B |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 8.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||
<meta name="description" content="A new Flutter project.">
|
||||
|
||||
<!-- iOS meta tags & icons -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-status-bar-style" content="black">
|
||||
<meta name="apple-mobile-web-app-title" content="example">
|
||||
<link rel="apple-touch-icon" href="/icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png"/>
|
||||
|
||||
<title>example</title>
|
||||
<link rel="manifest" href="/manifest.json">
|
||||
</head>
|
||||
<body>
|
||||
<!-- This script installs service_worker.js to provide PWA functionality to
|
||||
application. For more information, see:
|
||||
https://developers.google.com/web/fundamentals/primers/service-workers -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('/flutter_service_worker.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<script src="main.dart.js" type="application/javascript"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "example",
|
||||
"short_name": "example",
|
||||
"start_url": ".",
|
||||
"display": "minimal-ui",
|
||||
"background_color": "#0175C2",
|
||||
"theme_color": "#0175C2",
|
||||
"description": "A new Flutter project.",
|
||||
"orientation": "portrait-primary",
|
||||
"prefer_related_applications": false,
|
||||
"icons": [
|
||||
{
|
||||
"src": "icons/Icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "icons/Icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
flutter/ephemeral/
|
||||
|
||||
# Visual Studio user-specific files.
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# Visual Studio build-related files.
|
||||
x64/
|
||||
x86/
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!*.[Cc]ache/
|
||||
@@ -1,108 +0,0 @@
|
||||
# Project-level configuration.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(app_review_example LANGUAGES CXX)
|
||||
|
||||
# The name of the executable created for the application. Change this to change
|
||||
# the on-disk name of your application.
|
||||
set(BINARY_NAME "app_review_example")
|
||||
|
||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||
# versions of CMake.
|
||||
cmake_policy(VERSION 3.14...3.25)
|
||||
|
||||
# Define build configuration option.
|
||||
get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
|
||||
if(IS_MULTICONFIG)
|
||||
set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release"
|
||||
CACHE STRING "" FORCE)
|
||||
else()
|
||||
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
||||
set(CMAKE_BUILD_TYPE "Debug" CACHE
|
||||
STRING "Flutter build mode" FORCE)
|
||||
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
|
||||
"Debug" "Profile" "Release")
|
||||
endif()
|
||||
endif()
|
||||
# Define settings for the Profile build mode.
|
||||
set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}")
|
||||
set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}")
|
||||
set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}")
|
||||
|
||||
# Use Unicode for all projects.
|
||||
add_definitions(-DUNICODE -D_UNICODE)
|
||||
|
||||
# Compilation settings that should be applied to most targets.
|
||||
#
|
||||
# Be cautious about adding new options here, as plugins use this function by
|
||||
# default. In most cases, you should add new options to specific targets instead
|
||||
# of modifying this function.
|
||||
function(APPLY_STANDARD_SETTINGS TARGET)
|
||||
target_compile_features(${TARGET} PUBLIC cxx_std_17)
|
||||
target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100")
|
||||
target_compile_options(${TARGET} PRIVATE /EHsc)
|
||||
target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0")
|
||||
target_compile_definitions(${TARGET} PRIVATE "$<$<CONFIG:Debug>:_DEBUG>")
|
||||
endfunction()
|
||||
|
||||
# Flutter library and tool build rules.
|
||||
set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
|
||||
add_subdirectory(${FLUTTER_MANAGED_DIR})
|
||||
|
||||
# Application build; see runner/CMakeLists.txt.
|
||||
add_subdirectory("runner")
|
||||
|
||||
|
||||
# Generated plugin build rules, which manage building the plugins and adding
|
||||
# them to the application.
|
||||
include(flutter/generated_plugins.cmake)
|
||||
|
||||
|
||||
# === Installation ===
|
||||
# Support files are copied into place next to the executable, so that it can
|
||||
# run in place. This is done instead of making a separate bundle (as on Linux)
|
||||
# so that building and running from within Visual Studio will work.
|
||||
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||
# Make the "install" step default, as it's required to run.
|
||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||
endif()
|
||||
|
||||
set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
|
||||
set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")
|
||||
|
||||
install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
if(PLUGIN_BUNDLED_LIBRARIES)
|
||||
install(FILES "${PLUGIN_BUNDLED_LIBRARIES}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
endif()
|
||||
|
||||
# Copy the native assets provided by the build.dart from all packages.
|
||||
set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/")
|
||||
install(DIRECTORY "${NATIVE_ASSETS_DIR}"
|
||||
DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
|
||||
COMPONENT Runtime)
|
||||
|
||||
# Fully re-copy the assets directory on each build to avoid having stale files
|
||||
# from a previous install.
|
||||
set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
|
||||
install(CODE "
|
||||
file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
|
||||
" COMPONENT Runtime)
|
||||
install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
|
||||
DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
|
||||
|
||||
# Install the AOT library on non-Debug builds only.
|
||||
install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
|
||||
CONFIGURATIONS Profile;Release
|
||||
COMPONENT Runtime)
|
||||
@@ -1,109 +0,0 @@
|
||||
# This file controls Flutter-level build steps. It should not be edited.
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
|
||||
|
||||
# Configuration provided via flutter tool.
|
||||
include(${EPHEMERAL_DIR}/generated_config.cmake)
|
||||
|
||||
# TODO: Move the rest of this into files in ephemeral. See
|
||||
# https://github.com/flutter/flutter/issues/57146.
|
||||
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
|
||||
|
||||
# Set fallback configurations for older versions of the flutter tool.
|
||||
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
|
||||
set(FLUTTER_TARGET_PLATFORM "windows-x64")
|
||||
endif()
|
||||
|
||||
# === Flutter Library ===
|
||||
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
|
||||
|
||||
# Published to parent scope for install step.
|
||||
set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
|
||||
set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
|
||||
set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
|
||||
set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE)
|
||||
|
||||
list(APPEND FLUTTER_LIBRARY_HEADERS
|
||||
"flutter_export.h"
|
||||
"flutter_windows.h"
|
||||
"flutter_messenger.h"
|
||||
"flutter_plugin_registrar.h"
|
||||
"flutter_texture_registrar.h"
|
||||
)
|
||||
list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/")
|
||||
add_library(flutter INTERFACE)
|
||||
target_include_directories(flutter INTERFACE
|
||||
"${EPHEMERAL_DIR}"
|
||||
)
|
||||
target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib")
|
||||
add_dependencies(flutter flutter_assemble)
|
||||
|
||||
# === Wrapper ===
|
||||
list(APPEND CPP_WRAPPER_SOURCES_CORE
|
||||
"core_implementations.cc"
|
||||
"standard_codec.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_PLUGIN
|
||||
"plugin_registrar.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/")
|
||||
list(APPEND CPP_WRAPPER_SOURCES_APP
|
||||
"flutter_engine.cc"
|
||||
"flutter_view_controller.cc"
|
||||
)
|
||||
list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/")
|
||||
|
||||
# Wrapper sources needed for a plugin.
|
||||
add_library(flutter_wrapper_plugin STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_plugin)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
POSITION_INDEPENDENT_CODE ON)
|
||||
set_target_properties(flutter_wrapper_plugin PROPERTIES
|
||||
CXX_VISIBILITY_PRESET hidden)
|
||||
target_link_libraries(flutter_wrapper_plugin PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_plugin PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_plugin flutter_assemble)
|
||||
|
||||
# Wrapper sources needed for the runner.
|
||||
add_library(flutter_wrapper_app STATIC
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
apply_standard_settings(flutter_wrapper_app)
|
||||
target_link_libraries(flutter_wrapper_app PUBLIC flutter)
|
||||
target_include_directories(flutter_wrapper_app PUBLIC
|
||||
"${WRAPPER_ROOT}/include"
|
||||
)
|
||||
add_dependencies(flutter_wrapper_app flutter_assemble)
|
||||
|
||||
# === Flutter tool backend ===
|
||||
# _phony_ is a non-existent file to force this command to run every time,
|
||||
# since currently there's no way to get a full input/output list from the
|
||||
# flutter tool.
|
||||
set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_")
|
||||
set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE)
|
||||
add_custom_command(
|
||||
OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
${PHONY_OUTPUT}
|
||||
COMMAND ${CMAKE_COMMAND} -E env
|
||||
${FLUTTER_TOOL_ENVIRONMENT}
|
||||
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
|
||||
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
|
||||
VERBATIM
|
||||
)
|
||||
add_custom_target(flutter_assemble DEPENDS
|
||||
"${FLUTTER_LIBRARY}"
|
||||
${FLUTTER_LIBRARY_HEADERS}
|
||||
${CPP_WRAPPER_SOURCES_CORE}
|
||||
${CPP_WRAPPER_SOURCES_PLUGIN}
|
||||
${CPP_WRAPPER_SOURCES_APP}
|
||||
)
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//
|
||||
// Generated file. Do not edit.
|
||||
//
|
||||
|
||||
// clang-format off
|
||||
|
||||
#ifndef GENERATED_PLUGIN_REGISTRANT_
|
||||
#define GENERATED_PLUGIN_REGISTRANT_
|
||||
|
||||
#include <flutter/plugin_registry.h>
|
||||
|
||||
// Registers Flutter plugins.
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry);
|
||||
|
||||
#endif // GENERATED_PLUGIN_REGISTRANT_
|
||||
@@ -1,24 +0,0 @@
|
||||
#
|
||||
# Generated file, do not edit.
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
)
|
||||
|
||||
set(PLUGIN_BUNDLED_LIBRARIES)
|
||||
|
||||
foreach(plugin ${FLUTTER_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin})
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${plugin}_plugin>)
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
|
||||
endforeach(plugin)
|
||||
|
||||
foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
|
||||
add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin})
|
||||
list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
|
||||
endforeach(ffi_plugin)
|
||||
@@ -1,40 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(runner LANGUAGES CXX)
|
||||
|
||||
# Define the application target. To change its name, change BINARY_NAME in the
|
||||
# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer
|
||||
# work.
|
||||
#
|
||||
# Any new source files that you add to the application should be added here.
|
||||
add_executable(${BINARY_NAME} WIN32
|
||||
"flutter_window.cpp"
|
||||
"main.cpp"
|
||||
"utils.cpp"
|
||||
"win32_window.cpp"
|
||||
"${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
|
||||
"Runner.rc"
|
||||
"runner.exe.manifest"
|
||||
)
|
||||
|
||||
# Apply the standard set of build settings. This can be removed for applications
|
||||
# that need different build settings.
|
||||
apply_standard_settings(${BINARY_NAME})
|
||||
|
||||
# Add preprocessor definitions for the build version.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}")
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}")
|
||||
|
||||
# Disable Windows macros that collide with C++ standard library functions.
|
||||
target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX")
|
||||
|
||||
# Add dependency libraries and include directories. Add any application-specific
|
||||
# dependencies here.
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app)
|
||||
target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib")
|
||||
target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}")
|
||||
|
||||
# Run the Flutter tool portions of the build. This must not be removed.
|
||||
add_dependencies(${BINARY_NAME} flutter_assemble)
|
||||
@@ -1,121 +0,0 @@
|
||||
// Microsoft Visual C++ generated resource script.
|
||||
//
|
||||
#pragma code_page(65001)
|
||||
#include "resource.h"
|
||||
|
||||
#define APSTUDIO_READONLY_SYMBOLS
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 2 resource.
|
||||
//
|
||||
#include "winres.h"
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#undef APSTUDIO_READONLY_SYMBOLS
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// English (United States) resources
|
||||
|
||||
#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
|
||||
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
|
||||
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// TEXTINCLUDE
|
||||
//
|
||||
|
||||
1 TEXTINCLUDE
|
||||
BEGIN
|
||||
"resource.h\0"
|
||||
END
|
||||
|
||||
2 TEXTINCLUDE
|
||||
BEGIN
|
||||
"#include ""winres.h""\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
3 TEXTINCLUDE
|
||||
BEGIN
|
||||
"\r\n"
|
||||
"\0"
|
||||
END
|
||||
|
||||
#endif // APSTUDIO_INVOKED
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Icon
|
||||
//
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_APP_ICON ICON "resources\\app_icon.ico"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Version
|
||||
//
|
||||
|
||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||
#else
|
||||
#define VERSION_AS_NUMBER 1,0,0,0
|
||||
#endif
|
||||
|
||||
#if defined(FLUTTER_VERSION)
|
||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||
#else
|
||||
#define VERSION_AS_STRING "1.0.0"
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION VERSION_AS_NUMBER
|
||||
PRODUCTVERSION VERSION_AS_NUMBER
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS__WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904e4"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "com.example" "\0"
|
||||
VALUE "FileDescription", "app_review_example" "\0"
|
||||
VALUE "FileVersion", VERSION_AS_STRING "\0"
|
||||
VALUE "InternalName", "app_review_example" "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0"
|
||||
VALUE "OriginalFilename", "app_review_example.exe" "\0"
|
||||
VALUE "ProductName", "app_review_example" "\0"
|
||||
VALUE "ProductVersion", VERSION_AS_STRING "\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1252
|
||||
END
|
||||
END
|
||||
|
||||
#endif // English (United States) resources
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
#ifndef APSTUDIO_INVOKED
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Generated from the TEXTINCLUDE 3 resource.
|
||||
//
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
#endif // not APSTUDIO_INVOKED
|
||||
@@ -1,71 +0,0 @@
|
||||
#include "flutter_window.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "flutter/generated_plugin_registrant.h"
|
||||
|
||||
FlutterWindow::FlutterWindow(const flutter::DartProject& project)
|
||||
: project_(project) {}
|
||||
|
||||
FlutterWindow::~FlutterWindow() {}
|
||||
|
||||
bool FlutterWindow::OnCreate() {
|
||||
if (!Win32Window::OnCreate()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
// The size here must match the window dimensions to avoid unnecessary surface
|
||||
// creation / destruction in the startup path.
|
||||
flutter_controller_ = std::make_unique<flutter::FlutterViewController>(
|
||||
frame.right - frame.left, frame.bottom - frame.top, project_);
|
||||
// Ensure that basic setup of the controller was successful.
|
||||
if (!flutter_controller_->engine() || !flutter_controller_->view()) {
|
||||
return false;
|
||||
}
|
||||
RegisterPlugins(flutter_controller_->engine());
|
||||
SetChildContent(flutter_controller_->view()->GetNativeWindow());
|
||||
|
||||
flutter_controller_->engine()->SetNextFrameCallback([&]() {
|
||||
this->Show();
|
||||
});
|
||||
|
||||
// Flutter can complete the first frame before the "show window" callback is
|
||||
// registered. The following call ensures a frame is pending to ensure the
|
||||
// window is shown. It is a no-op if the first frame hasn't completed yet.
|
||||
flutter_controller_->ForceRedraw();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void FlutterWindow::OnDestroy() {
|
||||
if (flutter_controller_) {
|
||||
flutter_controller_ = nullptr;
|
||||
}
|
||||
|
||||
Win32Window::OnDestroy();
|
||||
}
|
||||
|
||||
LRESULT
|
||||
FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
// Give Flutter, including plugins, an opportunity to handle window messages.
|
||||
if (flutter_controller_) {
|
||||
std::optional<LRESULT> result =
|
||||
flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
|
||||
lparam);
|
||||
if (result) {
|
||||
return *result;
|
||||
}
|
||||
}
|
||||
|
||||
switch (message) {
|
||||
case WM_FONTCHANGE:
|
||||
flutter_controller_->engine()->ReloadSystemFonts();
|
||||
break;
|
||||
}
|
||||
|
||||
return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#ifndef RUNNER_FLUTTER_WINDOW_H_
|
||||
#define RUNNER_FLUTTER_WINDOW_H_
|
||||
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "win32_window.h"
|
||||
|
||||
// A window that does nothing but host a Flutter view.
|
||||
class FlutterWindow : public Win32Window {
|
||||
public:
|
||||
// Creates a new FlutterWindow hosting a Flutter view running |project|.
|
||||
explicit FlutterWindow(const flutter::DartProject& project);
|
||||
virtual ~FlutterWindow();
|
||||
|
||||
protected:
|
||||
// Win32Window:
|
||||
bool OnCreate() override;
|
||||
void OnDestroy() override;
|
||||
LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept override;
|
||||
|
||||
private:
|
||||
// The project to run.
|
||||
flutter::DartProject project_;
|
||||
|
||||
// The Flutter instance hosted by this window.
|
||||
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
|
||||
};
|
||||
|
||||
#endif // RUNNER_FLUTTER_WINDOW_H_
|
||||
@@ -1,43 +0,0 @@
|
||||
#include <flutter/dart_project.h>
|
||||
#include <flutter/flutter_view_controller.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "flutter_window.h"
|
||||
#include "utils.h"
|
||||
|
||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||
// Attach to console when present (e.g., 'flutter run') or create a
|
||||
// new console when running with a debugger.
|
||||
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
|
||||
CreateAndAttachConsole();
|
||||
}
|
||||
|
||||
// Initialize COM, so that it is available for use in the library and/or
|
||||
// plugins.
|
||||
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||
|
||||
flutter::DartProject project(L"data");
|
||||
|
||||
std::vector<std::string> command_line_arguments =
|
||||
GetCommandLineArguments();
|
||||
|
||||
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
|
||||
|
||||
FlutterWindow window(project);
|
||||
Win32Window::Point origin(10, 10);
|
||||
Win32Window::Size size(1280, 720);
|
||||
if (!window.Create(L"app_review_example", origin, size)) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
window.SetQuitOnClose(true);
|
||||
|
||||
::MSG msg;
|
||||
while (::GetMessage(&msg, nullptr, 0, 0)) {
|
||||
::TranslateMessage(&msg);
|
||||
::DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
::CoUninitialize();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ generated include file.
|
||||
// Used by Runner.rc
|
||||
//
|
||||
#define IDI_APP_ICON 101
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 102
|
||||
#define _APS_NEXT_COMMAND_VALUE 40001
|
||||
#define _APS_NEXT_CONTROL_VALUE 1001
|
||||
#define _APS_NEXT_SYMED_VALUE 101
|
||||
#endif
|
||||
#endif
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 and Windows 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
@@ -1,65 +0,0 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <flutter_windows.h>
|
||||
#include <io.h>
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
void CreateAndAttachConsole() {
|
||||
if (::AllocConsole()) {
|
||||
FILE *unused;
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stdout)) {
|
||||
_dup2(_fileno(stdout), 1);
|
||||
}
|
||||
if (freopen_s(&unused, "CONOUT$", "w", stderr)) {
|
||||
_dup2(_fileno(stdout), 2);
|
||||
}
|
||||
std::ios::sync_with_stdio();
|
||||
FlutterDesktopResyncOutputStreams();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> GetCommandLineArguments() {
|
||||
// Convert the UTF-16 command line arguments to UTF-8 for the Engine to use.
|
||||
int argc;
|
||||
wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
|
||||
if (argv == nullptr) {
|
||||
return std::vector<std::string>();
|
||||
}
|
||||
|
||||
std::vector<std::string> command_line_arguments;
|
||||
|
||||
// Skip the first argument as it's the binary name.
|
||||
for (int i = 1; i < argc; i++) {
|
||||
command_line_arguments.push_back(Utf8FromUtf16(argv[i]));
|
||||
}
|
||||
|
||||
::LocalFree(argv);
|
||||
|
||||
return command_line_arguments;
|
||||
}
|
||||
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string) {
|
||||
if (utf16_string == nullptr) {
|
||||
return std::string();
|
||||
}
|
||||
unsigned int target_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
-1, nullptr, 0, nullptr, nullptr)
|
||||
-1; // remove the trailing null character
|
||||
int input_length = (int)wcslen(utf16_string);
|
||||
std::string utf8_string;
|
||||
if (target_length == 0 || target_length > utf8_string.max_size()) {
|
||||
return utf8_string;
|
||||
}
|
||||
utf8_string.resize(target_length);
|
||||
int converted_length = ::WideCharToMultiByte(
|
||||
CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string,
|
||||
input_length, utf8_string.data(), target_length, nullptr, nullptr);
|
||||
if (converted_length == 0) {
|
||||
return std::string();
|
||||
}
|
||||
return utf8_string;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
#ifndef RUNNER_UTILS_H_
|
||||
#define RUNNER_UTILS_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Creates a console for the process, and redirects stdout and stderr to
|
||||
// it for both the runner and the Flutter library.
|
||||
void CreateAndAttachConsole();
|
||||
|
||||
// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string
|
||||
// encoded in UTF-8. Returns an empty std::string on failure.
|
||||
std::string Utf8FromUtf16(const wchar_t* utf16_string);
|
||||
|
||||
// Gets the command line arguments passed in as a std::vector<std::string>,
|
||||
// encoded in UTF-8. Returns an empty std::vector<std::string> on failure.
|
||||
std::vector<std::string> GetCommandLineArguments();
|
||||
|
||||
#endif // RUNNER_UTILS_H_
|
||||
@@ -1,288 +0,0 @@
|
||||
#include "win32_window.h"
|
||||
|
||||
#include <dwmapi.h>
|
||||
#include <flutter_windows.h>
|
||||
|
||||
#include "resource.h"
|
||||
|
||||
namespace {
|
||||
|
||||
/// Window attribute that enables dark mode window decorations.
|
||||
///
|
||||
/// Redefined in case the developer's machine has a Windows SDK older than
|
||||
/// version 10.0.22000.0.
|
||||
/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
|
||||
#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
|
||||
#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
|
||||
#endif
|
||||
|
||||
constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW";
|
||||
|
||||
/// Registry key for app theme preference.
|
||||
///
|
||||
/// A value of 0 indicates apps should use dark mode. A non-zero or missing
|
||||
/// value indicates apps should use light mode.
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegKey[] =
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
|
||||
constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme";
|
||||
|
||||
// The number of Win32Window objects that currently exist.
|
||||
static int g_active_window_count = 0;
|
||||
|
||||
using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd);
|
||||
|
||||
// Scale helper to convert logical scaler values to physical using passed in
|
||||
// scale factor
|
||||
int Scale(int source, double scale_factor) {
|
||||
return static_cast<int>(source * scale_factor);
|
||||
}
|
||||
|
||||
// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module.
|
||||
// This API is only needed for PerMonitor V1 awareness mode.
|
||||
void EnableFullDpiSupportIfAvailable(HWND hwnd) {
|
||||
HMODULE user32_module = LoadLibraryA("User32.dll");
|
||||
if (!user32_module) {
|
||||
return;
|
||||
}
|
||||
auto enable_non_client_dpi_scaling =
|
||||
reinterpret_cast<EnableNonClientDpiScaling*>(
|
||||
GetProcAddress(user32_module, "EnableNonClientDpiScaling"));
|
||||
if (enable_non_client_dpi_scaling != nullptr) {
|
||||
enable_non_client_dpi_scaling(hwnd);
|
||||
}
|
||||
FreeLibrary(user32_module);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Manages the Win32Window's window class registration.
|
||||
class WindowClassRegistrar {
|
||||
public:
|
||||
~WindowClassRegistrar() = default;
|
||||
|
||||
// Returns the singleton registrar instance.
|
||||
static WindowClassRegistrar* GetInstance() {
|
||||
if (!instance_) {
|
||||
instance_ = new WindowClassRegistrar();
|
||||
}
|
||||
return instance_;
|
||||
}
|
||||
|
||||
// Returns the name of the window class, registering the class if it hasn't
|
||||
// previously been registered.
|
||||
const wchar_t* GetWindowClass();
|
||||
|
||||
// Unregisters the window class. Should only be called if there are no
|
||||
// instances of the window.
|
||||
void UnregisterWindowClass();
|
||||
|
||||
private:
|
||||
WindowClassRegistrar() = default;
|
||||
|
||||
static WindowClassRegistrar* instance_;
|
||||
|
||||
bool class_registered_ = false;
|
||||
};
|
||||
|
||||
WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr;
|
||||
|
||||
const wchar_t* WindowClassRegistrar::GetWindowClass() {
|
||||
if (!class_registered_) {
|
||||
WNDCLASS window_class{};
|
||||
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||
window_class.lpszClassName = kWindowClassName;
|
||||
window_class.style = CS_HREDRAW | CS_VREDRAW;
|
||||
window_class.cbClsExtra = 0;
|
||||
window_class.cbWndExtra = 0;
|
||||
window_class.hInstance = GetModuleHandle(nullptr);
|
||||
window_class.hIcon =
|
||||
LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON));
|
||||
window_class.hbrBackground = 0;
|
||||
window_class.lpszMenuName = nullptr;
|
||||
window_class.lpfnWndProc = Win32Window::WndProc;
|
||||
RegisterClass(&window_class);
|
||||
class_registered_ = true;
|
||||
}
|
||||
return kWindowClassName;
|
||||
}
|
||||
|
||||
void WindowClassRegistrar::UnregisterWindowClass() {
|
||||
UnregisterClass(kWindowClassName, nullptr);
|
||||
class_registered_ = false;
|
||||
}
|
||||
|
||||
Win32Window::Win32Window() {
|
||||
++g_active_window_count;
|
||||
}
|
||||
|
||||
Win32Window::~Win32Window() {
|
||||
--g_active_window_count;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
bool Win32Window::Create(const std::wstring& title,
|
||||
const Point& origin,
|
||||
const Size& size) {
|
||||
Destroy();
|
||||
|
||||
const wchar_t* window_class =
|
||||
WindowClassRegistrar::GetInstance()->GetWindowClass();
|
||||
|
||||
const POINT target_point = {static_cast<LONG>(origin.x),
|
||||
static_cast<LONG>(origin.y)};
|
||||
HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST);
|
||||
UINT dpi = FlutterDesktopGetDpiForMonitor(monitor);
|
||||
double scale_factor = dpi / 96.0;
|
||||
|
||||
HWND window = CreateWindow(
|
||||
window_class, title.c_str(), WS_OVERLAPPEDWINDOW,
|
||||
Scale(origin.x, scale_factor), Scale(origin.y, scale_factor),
|
||||
Scale(size.width, scale_factor), Scale(size.height, scale_factor),
|
||||
nullptr, nullptr, GetModuleHandle(nullptr), this);
|
||||
|
||||
if (!window) {
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateTheme(window);
|
||||
|
||||
return OnCreate();
|
||||
}
|
||||
|
||||
bool Win32Window::Show() {
|
||||
return ShowWindow(window_handle_, SW_SHOWNORMAL);
|
||||
}
|
||||
|
||||
// static
|
||||
LRESULT CALLBACK Win32Window::WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto window_struct = reinterpret_cast<CREATESTRUCT*>(lparam);
|
||||
SetWindowLongPtr(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(window_struct->lpCreateParams));
|
||||
|
||||
auto that = static_cast<Win32Window*>(window_struct->lpCreateParams);
|
||||
EnableFullDpiSupportIfAvailable(window);
|
||||
that->window_handle_ = window;
|
||||
} else if (Win32Window* that = GetThisFromHandle(window)) {
|
||||
return that->MessageHandler(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
return DefWindowProc(window, message, wparam, lparam);
|
||||
}
|
||||
|
||||
LRESULT
|
||||
Win32Window::MessageHandler(HWND hwnd,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept {
|
||||
switch (message) {
|
||||
case WM_DESTROY:
|
||||
window_handle_ = nullptr;
|
||||
Destroy();
|
||||
if (quit_on_close_) {
|
||||
PostQuitMessage(0);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DPICHANGED: {
|
||||
auto newRectSize = reinterpret_cast<RECT*>(lparam);
|
||||
LONG newWidth = newRectSize->right - newRectSize->left;
|
||||
LONG newHeight = newRectSize->bottom - newRectSize->top;
|
||||
|
||||
SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth,
|
||||
newHeight, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
|
||||
return 0;
|
||||
}
|
||||
case WM_SIZE: {
|
||||
RECT rect = GetClientArea();
|
||||
if (child_content_ != nullptr) {
|
||||
// Size and position the child window.
|
||||
MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (child_content_ != nullptr) {
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DWMCOLORIZATIONCOLORCHANGED:
|
||||
UpdateTheme(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return DefWindowProc(window_handle_, message, wparam, lparam);
|
||||
}
|
||||
|
||||
void Win32Window::Destroy() {
|
||||
OnDestroy();
|
||||
|
||||
if (window_handle_) {
|
||||
DestroyWindow(window_handle_);
|
||||
window_handle_ = nullptr;
|
||||
}
|
||||
if (g_active_window_count == 0) {
|
||||
WindowClassRegistrar::GetInstance()->UnregisterWindowClass();
|
||||
}
|
||||
}
|
||||
|
||||
Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept {
|
||||
return reinterpret_cast<Win32Window*>(
|
||||
GetWindowLongPtr(window, GWLP_USERDATA));
|
||||
}
|
||||
|
||||
void Win32Window::SetChildContent(HWND content) {
|
||||
child_content_ = content;
|
||||
SetParent(content, window_handle_);
|
||||
RECT frame = GetClientArea();
|
||||
|
||||
MoveWindow(content, frame.left, frame.top, frame.right - frame.left,
|
||||
frame.bottom - frame.top, true);
|
||||
|
||||
SetFocus(child_content_);
|
||||
}
|
||||
|
||||
RECT Win32Window::GetClientArea() {
|
||||
RECT frame;
|
||||
GetClientRect(window_handle_, &frame);
|
||||
return frame;
|
||||
}
|
||||
|
||||
HWND Win32Window::GetHandle() {
|
||||
return window_handle_;
|
||||
}
|
||||
|
||||
void Win32Window::SetQuitOnClose(bool quit_on_close) {
|
||||
quit_on_close_ = quit_on_close;
|
||||
}
|
||||
|
||||
bool Win32Window::OnCreate() {
|
||||
// No-op; provided for subclasses.
|
||||
return true;
|
||||
}
|
||||
|
||||
void Win32Window::OnDestroy() {
|
||||
// No-op; provided for subclasses.
|
||||
}
|
||||
|
||||
void Win32Window::UpdateTheme(HWND const window) {
|
||||
DWORD light_mode;
|
||||
DWORD light_mode_size = sizeof(light_mode);
|
||||
LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey,
|
||||
kGetPreferredBrightnessRegValue,
|
||||
RRF_RT_REG_DWORD, nullptr, &light_mode,
|
||||
&light_mode_size);
|
||||
|
||||
if (result == ERROR_SUCCESS) {
|
||||
BOOL enable_dark_mode = light_mode == 0;
|
||||
DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&enable_dark_mode, sizeof(enable_dark_mode));
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
#ifndef RUNNER_WIN32_WINDOW_H_
|
||||
#define RUNNER_WIN32_WINDOW_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
// A class abstraction for a high DPI-aware Win32 Window. Intended to be
|
||||
// inherited from by classes that wish to specialize with custom
|
||||
// rendering and input handling
|
||||
class Win32Window {
|
||||
public:
|
||||
struct Point {
|
||||
unsigned int x;
|
||||
unsigned int y;
|
||||
Point(unsigned int x, unsigned int y) : x(x), y(y) {}
|
||||
};
|
||||
|
||||
struct Size {
|
||||
unsigned int width;
|
||||
unsigned int height;
|
||||
Size(unsigned int width, unsigned int height)
|
||||
: width(width), height(height) {}
|
||||
};
|
||||
|
||||
Win32Window();
|
||||
virtual ~Win32Window();
|
||||
|
||||
// Creates a win32 window with |title| that is positioned and sized using
|
||||
// |origin| and |size|. New windows are created on the default monitor. Window
|
||||
// sizes are specified to the OS in physical pixels, hence to ensure a
|
||||
// consistent size this function will scale the inputted width and height as
|
||||
// as appropriate for the default monitor. The window is invisible until
|
||||
// |Show| is called. Returns true if the window was created successfully.
|
||||
bool Create(const std::wstring& title, const Point& origin, const Size& size);
|
||||
|
||||
// Show the current window. Returns true if the window was successfully shown.
|
||||
bool Show();
|
||||
|
||||
// Release OS resources associated with window.
|
||||
void Destroy();
|
||||
|
||||
// Inserts |content| into the window tree.
|
||||
void SetChildContent(HWND content);
|
||||
|
||||
// Returns the backing Window handle to enable clients to set icon and other
|
||||
// window properties. Returns nullptr if the window has been destroyed.
|
||||
HWND GetHandle();
|
||||
|
||||
// If true, closing this window will quit the application.
|
||||
void SetQuitOnClose(bool quit_on_close);
|
||||
|
||||
// Return a RECT representing the bounds of the current client area.
|
||||
RECT GetClientArea();
|
||||
|
||||
protected:
|
||||
// Processes and route salient window messages for mouse handling,
|
||||
// size change and DPI. Delegates handling of these to member overloads that
|
||||
// inheriting classes can handle.
|
||||
virtual LRESULT MessageHandler(HWND window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Called when CreateAndShow is called, allowing subclass window-related
|
||||
// setup. Subclasses should return false if setup fails.
|
||||
virtual bool OnCreate();
|
||||
|
||||
// Called when Destroy is called.
|
||||
virtual void OnDestroy();
|
||||
|
||||
private:
|
||||
friend class WindowClassRegistrar;
|
||||
|
||||
// OS callback called by message pump. Handles the WM_NCCREATE message which
|
||||
// is passed when the non-client area is being created and enables automatic
|
||||
// non-client DPI scaling so that the non-client area automatically
|
||||
// responds to changes in DPI. All other messages are handled by
|
||||
// MessageHandler.
|
||||
static LRESULT CALLBACK WndProc(HWND const window,
|
||||
UINT const message,
|
||||
WPARAM const wparam,
|
||||
LPARAM const lparam) noexcept;
|
||||
|
||||
// Retrieves a class instance pointer for |window|
|
||||
static Win32Window* GetThisFromHandle(HWND const window) noexcept;
|
||||
|
||||
// Update the window frame's theme to match the system theme.
|
||||
static void UpdateTheme(HWND const window);
|
||||
|
||||
bool quit_on_close_ = false;
|
||||
|
||||
// window handle for top level window.
|
||||
HWND window_handle_ = nullptr;
|
||||
|
||||
// window handle for hosted content.
|
||||
HWND child_content_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // RUNNER_WIN32_WINDOW_H_
|
||||
@@ -1,4 +0,0 @@
|
||||
#import <Flutter/Flutter.h>
|
||||
|
||||
@interface AppReviewPlugin : NSObject<FlutterPlugin>
|
||||
@end
|
||||
@@ -1,8 +0,0 @@
|
||||
#import "AppReviewPlugin.h"
|
||||
#import <app_review/app_review-Swift.h>
|
||||
|
||||
@implementation AppReviewPlugin
|
||||
+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
|
||||
[SwiftAppReviewPlugin registerWithRegistrar:registrar];
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,134 @@
|
||||
import Flutter
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
|
||||
public class AppReviewPlugin: NSObject, FlutterPlugin, AppReviewApi {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = AppReviewPlugin()
|
||||
AppReviewApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance)
|
||||
}
|
||||
|
||||
public func requestReview(testMode: Bool, completion: @escaping (Result<String?, Error>) -> Void) {
|
||||
if #available(iOS 16.0, *) {
|
||||
DispatchQueue.main.async {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene,
|
||||
let keyWindow = windowScene.windows.first(where: { $0.isKeyWindow }) {
|
||||
let controller = UIHostingController(rootView: ReviewRequestView())
|
||||
controller.view.isHidden = true
|
||||
controller.view.frame = .zero
|
||||
keyWindow.addSubview(controller.view)
|
||||
// Triggering onAppear by adding to hierarchy.
|
||||
}
|
||||
}
|
||||
completion(.success("Requested Review via SwiftUI"))
|
||||
} else if #available(iOS 14.0, *) {
|
||||
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
||||
SKStoreReviewController.requestReview(in: scene)
|
||||
completion(.success("Requested Review"))
|
||||
} else {
|
||||
SKStoreReviewController.requestReview()
|
||||
completion(.success("Requested Review"))
|
||||
}
|
||||
} else if #available(iOS 10.3, *) {
|
||||
SKStoreReviewController.requestReview()
|
||||
completion(.success("Requested Review"))
|
||||
} else {
|
||||
completion(.success("Review not available"))
|
||||
}
|
||||
}
|
||||
|
||||
public func isRequestReviewAvailable(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
if #available(iOS 10.3, *) {
|
||||
completion(.success(true))
|
||||
} else {
|
||||
completion(.success(false))
|
||||
}
|
||||
}
|
||||
|
||||
public func getBundleId(completion: @escaping (Result<String, Error>) -> Void) {
|
||||
completion(.success(Bundle.main.bundleIdentifier ?? ""))
|
||||
}
|
||||
|
||||
public func openStoreListing(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let storeId = storeId, !storeId.isEmpty else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 404, userInfo: [NSLocalizedDescriptionKey: "Store ID is missing"])))
|
||||
return
|
||||
}
|
||||
let urlString = "https://apps.apple.com/app/id\(storeId)"
|
||||
if let url = URL(string: urlString) {
|
||||
UIApplication.shared.open(url, options: [:]) { success in
|
||||
if success {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to open store listing"])))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid Store URL"])))
|
||||
}
|
||||
}
|
||||
|
||||
public func openAppStoreReview(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let storeId = storeId, !storeId.isEmpty else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 404, userInfo: [NSLocalizedDescriptionKey: "Store ID is missing"])))
|
||||
return
|
||||
}
|
||||
let urlString = "https://apps.apple.com/app/id\(storeId)?action=write-review"
|
||||
if let url = URL(string: urlString) {
|
||||
UIApplication.shared.open(url, options: [:]) { success in
|
||||
if success {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to open App Store review"])))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid Store URL"])))
|
||||
}
|
||||
}
|
||||
|
||||
public func lookupAppId(bundleId: String, countryCode: String?, completion: @escaping (Result<String?, Error>) -> Void) {
|
||||
let country = countryCode ?? ""
|
||||
let urlString = "https://itunes.apple.com/\(country)/lookup?bundleId=\(bundleId)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid Lookup URL"])))
|
||||
return
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(.success(nil))
|
||||
return
|
||||
}
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let results = json["results"] as? [[String: Any]],
|
||||
let firstResult = results.first,
|
||||
let trackId = firstResult["trackId"] as? Int {
|
||||
completion(.success(String(trackId)))
|
||||
} else {
|
||||
completion(.success(nil))
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.0, *)
|
||||
struct ReviewRequestView: View {
|
||||
@Environment(\.requestReview) var requestReview
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
.onAppear {
|
||||
requestReview()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import Flutter
|
||||
#elseif os(macOS)
|
||||
import FlutterMacOS
|
||||
#else
|
||||
#error("Unsupported platform.")
|
||||
#endif
|
||||
|
||||
/// Error class for passing custom error details to Dart side.
|
||||
final class PigeonError: Error {
|
||||
let code: String
|
||||
let message: String?
|
||||
let details: Sendable?
|
||||
|
||||
init(code: String, message: String?, details: Sendable?) {
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.details = details
|
||||
}
|
||||
|
||||
var localizedDescription: String {
|
||||
return
|
||||
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
|
||||
}
|
||||
}
|
||||
|
||||
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||
return [result]
|
||||
}
|
||||
|
||||
private func wrapError(_ error: Any) -> [Any?] {
|
||||
if let pigeonError = error as? PigeonError {
|
||||
return [
|
||||
pigeonError.code,
|
||||
pigeonError.message,
|
||||
pigeonError.details,
|
||||
]
|
||||
}
|
||||
if let flutterError = error as? FlutterError {
|
||||
return [
|
||||
flutterError.code,
|
||||
flutterError.message,
|
||||
flutterError.details,
|
||||
]
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
private func isNullish(_ value: Any?) -> Bool {
|
||||
return value is NSNull || value == nil
|
||||
}
|
||||
|
||||
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
if value is NSNull { return nil }
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
|
||||
private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||
}
|
||||
|
||||
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||
}
|
||||
|
||||
private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||
override func reader(with data: Data) -> FlutterStandardReader {
|
||||
return MessagesPigeonCodecReader(data: data)
|
||||
}
|
||||
|
||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||
return MessagesPigeonCodecWriter(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
|
||||
}
|
||||
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol AppReviewApi {
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
func requestReview(testMode: Bool, completion: @escaping (Result<String?, Error>) -> Void)
|
||||
/// Check if [requestReview] feature available.
|
||||
func isRequestReviewAvailable(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
/// Opens the store listing for the specified app.
|
||||
///
|
||||
/// [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
func openStoreListing(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
/// Opens the App Store review page (iOS/macOS only).
|
||||
///
|
||||
/// [storeId] is the App ID.
|
||||
func openAppStoreReview(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
/// Returns package name for application.
|
||||
func getBundleId(completion: @escaping (Result<String, Error>) -> Void)
|
||||
/// Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
///
|
||||
/// [bundleId] is the bundle identifier to look up.
|
||||
/// [countryCode] is the optional country code.
|
||||
func lookupAppId(bundleId: String, countryCode: String?, completion: @escaping (Result<String?, Error>) -> Void)
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
class AppReviewApiSetup {
|
||||
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
|
||||
/// Sets up an instance of `AppReviewApi` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AppReviewApi?, messageChannelSuffix: String = "") {
|
||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
let requestReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.requestReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
requestReviewChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let testModeArg = args[0] as! Bool
|
||||
api.requestReview(testMode: testModeArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestReviewChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Check if [requestReview] feature available.
|
||||
let isRequestReviewAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.isRequestReviewAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
isRequestReviewAvailableChannel.setMessageHandler { _, reply in
|
||||
api.isRequestReviewAvailable { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isRequestReviewAvailableChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Opens the store listing for the specified app.
|
||||
///
|
||||
/// [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
let openStoreListingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.openStoreListing\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
openStoreListingChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let storeIdArg: String? = nilOrValue(args[0])
|
||||
api.openStoreListing(storeId: storeIdArg) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openStoreListingChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Opens the App Store review page (iOS/macOS only).
|
||||
///
|
||||
/// [storeId] is the App ID.
|
||||
let openAppStoreReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.openAppStoreReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
openAppStoreReviewChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let storeIdArg: String? = nilOrValue(args[0])
|
||||
api.openAppStoreReview(storeId: storeIdArg) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openAppStoreReviewChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Returns package name for application.
|
||||
let getBundleIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.getBundleId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
getBundleIdChannel.setMessageHandler { _, reply in
|
||||
api.getBundleId { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getBundleIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
///
|
||||
/// [bundleId] is the bundle identifier to look up.
|
||||
/// [countryCode] is the optional country code.
|
||||
let lookupAppIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.lookupAppId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
lookupAppIdChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let bundleIdArg = args[0] as! String
|
||||
let countryCodeArg: String? = nilOrValue(args[1])
|
||||
api.lookupAppId(bundleId: bundleIdArg, countryCode: countryCodeArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lookupAppIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
import StoreKit
|
||||
|
||||
public class SwiftAppReviewPlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "app_review", binaryMessenger: registrar.messenger())
|
||||
let instance = SwiftAppReviewPlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
var appID = "com.apple.appstore" //Your App ID on App Store
|
||||
if let bundleIdentifier = Bundle.main.bundleIdentifier {
|
||||
appID = bundleIdentifier
|
||||
}
|
||||
|
||||
switch (call.method) {
|
||||
case "requestReview":
|
||||
//App Store Review
|
||||
if #available(iOS 10.3, *) {
|
||||
SKStoreReviewController.requestReview() // Requesting alert view for getting rating from the user.
|
||||
result("Later than iOS 10.3 the App will Request the App Review, Users can turn this off in settings for all apps, Apple will manage when to request the review from the user. In Debug it will always show. Requesting review for: " + appID)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
result("Prior to iOS 10.3 App Review from App is not available. You should go to Store Page of App: " + appID + ". If the app is not published the app will not be found.")
|
||||
}
|
||||
case "isRequestReviewAvailable":
|
||||
if #available(iOS 10.3, *) {
|
||||
result("1")
|
||||
} else {
|
||||
result("0")
|
||||
}
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -13,10 +13,10 @@ Request and Write Reviews and Open Store Listing for Android/iOS in Flutter
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.public_header_files = 'Classes/**/*.h'
|
||||
s.dependency 'Flutter'
|
||||
s.swift_version = "5"
|
||||
|
||||
s.ios.deployment_target = '11.0'
|
||||
end
|
||||
s.platform = :ios, '12.0'
|
||||
|
||||
# Flutter.framework does not contain a i386 slice.
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
|
||||
@@ -1,206 +1,37 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'src/exceptions.dart';
|
||||
import 'src/messages.g.dart';
|
||||
|
||||
class AppReview {
|
||||
static const Duration kDefaultDuration = Duration(minutes: 5);
|
||||
static const MethodChannel _channel = MethodChannel('app_review');
|
||||
static AppReviewApi _api = AppReviewApi();
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Maintain original interface
|
||||
// Public Interface
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
/// Returns package name for application.
|
||||
static Future<String?> get getAppID => getBundleName();
|
||||
static Future<String?> getAppId() => getBundleName();
|
||||
|
||||
/// Returns package name for application.
|
||||
static Future<String?> getBundleName() async {
|
||||
if (_appBundle != null) {
|
||||
return _appBundle;
|
||||
}
|
||||
try {
|
||||
_appBundle = await _api.getBundleId();
|
||||
return _appBundle;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Apple ID for iOS application.
|
||||
///
|
||||
/// If there is no such application in App Store - returns empty string.
|
||||
static Future<String?> get getiOSAppID => getIosAppId();
|
||||
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
static Future<String?> get requestReview async {
|
||||
if (Platform.isIOS) {
|
||||
return openIosReview();
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
return openAndroidReview();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
static Future<Timer> requestReviewDelayed([Duration? duration]) async =>
|
||||
Timer(duration ?? kDefaultDuration, () => requestReview);
|
||||
|
||||
/// Check if [requestReview] feature available.
|
||||
static Future<bool> get isRequestReviewAvailable async {
|
||||
if (Platform.isIOS || Platform.isAndroid) {
|
||||
try {
|
||||
final result =
|
||||
await _channel.invokeMethod<String>('isRequestReviewAvailable');
|
||||
return result == '1';
|
||||
} finally {}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Open store page with action write review.
|
||||
///
|
||||
/// Supported only for iOS, on Android [storeListing] will be executed.
|
||||
static Future<String?> get writeReview async {
|
||||
if (Platform.isIOS) {
|
||||
return openIosReview(compose: true);
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
return openAndroidReview();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Navigates to Store Listing in Google Play/App Store.
|
||||
///
|
||||
/// Returns string with details message.
|
||||
static Future<String?> get storeListing async {
|
||||
if (Platform.isIOS) {
|
||||
return openAppStore();
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
return openGooglePlay();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Helper methods (added by @shinsenter)
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
static PackageInfo? _packageInfo;
|
||||
static String? _appCountry;
|
||||
static String? _appBundle;
|
||||
static String? _appId;
|
||||
|
||||
/// It would be great if I could add country code into AppStore lookup URL.
|
||||
/// Eg: AppReview.setCountryCode('jp');
|
||||
static void setCountryCode(String code) =>
|
||||
_appCountry = code.isEmpty ? null : code;
|
||||
|
||||
/// Require app review for iOS
|
||||
static Future<String?> openIosReview({
|
||||
String? appId,
|
||||
bool compose = false,
|
||||
}) async {
|
||||
if (compose) {
|
||||
final id = appId ?? (await getIosAppId()) ?? '';
|
||||
// New format: https://apps.apple.com/app/idYOURAPPSTOREID?action=write-review
|
||||
final reviewUrl = 'apps.apple.com/app/id$id';
|
||||
final uri = Uri.parse('https://$reviewUrl?action=write-review');
|
||||
|
||||
if (await canLaunchUrl(uri)) {
|
||||
debugPrint('launching store page');
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
return 'Launched App Store Directly: $reviewUrl';
|
||||
}
|
||||
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
return 'Launched App Store: $reviewUrl';
|
||||
}
|
||||
|
||||
try {
|
||||
return _channel.invokeMethod<String>('requestReview');
|
||||
} finally {}
|
||||
}
|
||||
|
||||
/// Require app review for Android
|
||||
static Future<String?> openAndroidReview() {
|
||||
try {
|
||||
return _channel.invokeMethod<String>('requestReview');
|
||||
} on Error {
|
||||
return openGooglePlay();
|
||||
}
|
||||
}
|
||||
|
||||
/// Open in AppStore
|
||||
static Future<String> openAppStore({String? fallbackUrl}) async {
|
||||
final appId = await getIosAppId() ?? '';
|
||||
|
||||
if (appId.isNotEmpty) {
|
||||
launchUrl(Uri.parse('https://apps.apple.com/app/id$appId'),
|
||||
mode: LaunchMode.externalApplication);
|
||||
return 'Launched App Store';
|
||||
}
|
||||
|
||||
if (fallbackUrl != null) {
|
||||
launchUrl(Uri.parse(fallbackUrl), mode: LaunchMode.externalApplication);
|
||||
return 'Launched App Store via $fallbackUrl';
|
||||
}
|
||||
|
||||
return 'Not found in App Store';
|
||||
}
|
||||
|
||||
/// Open in GooglePlay
|
||||
static Future<String> openGooglePlay({String? fallbackUrl}) async {
|
||||
final bundle = await getBundleName() ?? '';
|
||||
final markerUrl = 'market://details?id=$bundle';
|
||||
final uri = Uri.parse(markerUrl);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
debugPrint('launching store page');
|
||||
launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
return 'Launched Google Play Directly: $bundle';
|
||||
}
|
||||
|
||||
if (fallbackUrl != null) {
|
||||
launchUrl(Uri.parse(fallbackUrl), mode: LaunchMode.externalApplication);
|
||||
return 'Launched Google Play via $fallbackUrl';
|
||||
}
|
||||
|
||||
launchUrl(
|
||||
Uri.parse('https://play.google.com/store/apps/details?id=$bundle'));
|
||||
return 'Launched Google Play: $bundle';
|
||||
}
|
||||
|
||||
/// Lazy load package info instance
|
||||
static Future<PackageInfo?> getPackageInfo() async {
|
||||
_packageInfo ??= await PackageInfo.fromPlatform();
|
||||
|
||||
debugPrint('App Name: ${_packageInfo!.appName}\n'
|
||||
'Package Name: ${_packageInfo!.packageName}\n'
|
||||
'Version: ${_packageInfo!.version}\n'
|
||||
'Build Number: ${_packageInfo!.buildNumber}');
|
||||
|
||||
return _packageInfo;
|
||||
}
|
||||
|
||||
/// Get app bundle name
|
||||
static Future<String?> getBundleName() async {
|
||||
_appBundle ??= (await getPackageInfo())?.packageName ?? '';
|
||||
return _appBundle;
|
||||
}
|
||||
|
||||
/// Get app's AppStore ID (public app only)
|
||||
static Future<String?> getIosAppId({
|
||||
String? countryCode,
|
||||
String? bundleId,
|
||||
@@ -223,15 +54,7 @@ class AppReview {
|
||||
|
||||
if (id.isNotEmpty) {
|
||||
try {
|
||||
final result = await http
|
||||
.get(Uri.parse(
|
||||
'https://itunes.apple.com/$country/lookup?bundleId=$id'))
|
||||
.timeout(const Duration(seconds: 5));
|
||||
final Map json = jsonDecode(result.body);
|
||||
final List results = json['results'] as List;
|
||||
if (results.isNotEmpty) {
|
||||
appId = results[0]['trackId']?.toString();
|
||||
}
|
||||
appId = await _api.lookupAppId(id, country);
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching app ID: $e');
|
||||
} finally {
|
||||
@@ -245,4 +68,159 @@ class AppReview {
|
||||
|
||||
return appId ?? '';
|
||||
}
|
||||
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Throws [AppReviewException] if request fails.
|
||||
static Future<void> requestReview({
|
||||
bool useAndroidTestMode = false,
|
||||
}) async {
|
||||
final result = await _api.requestReview(useAndroidTestMode);
|
||||
if (result != null && result.toLowerCase().contains("not available")) {
|
||||
throw AppReviewUnavailableException(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if [requestReview] feature available.
|
||||
static Future<bool> isRequestReviewAvailable() async {
|
||||
if (Platform.isIOS || Platform.isAndroid || Platform.isMacOS) {
|
||||
try {
|
||||
return await _api.isRequestReviewAvailable();
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Open store page with action write review.
|
||||
///
|
||||
/// Supported only for iOS, on Android [storeListing] will be executed.
|
||||
///
|
||||
/// [appStoreId] - App ID for iOS App Store (e.g. 1234567890)
|
||||
/// [playStoreId] - Package name for Google Play Store (e.g. com.example.app) on Android.
|
||||
static Future<void> writeReview({
|
||||
String? appStoreId,
|
||||
String? playStoreId,
|
||||
bool useAndroidTestMode = false,
|
||||
}) async {
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
await _openIosReview(
|
||||
appId: appStoreId,
|
||||
compose: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await _openAndroidReview(
|
||||
appId: playStoreId,
|
||||
useAndroidTestMode: useAndroidTestMode,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigates to Store Listing in Google Play/App Store.
|
||||
///
|
||||
/// [appStoreId] - App ID for iOS App Store (e.g. 1234567890)
|
||||
/// [playStoreId] - Package name for Google Play Store (e.g. com.example.app) on Android.
|
||||
static Future<void> storeListing({
|
||||
String? appStoreId,
|
||||
String? playStoreId,
|
||||
}) async {
|
||||
if (Platform.isIOS || Platform.isMacOS) {
|
||||
await openAppStore(appId: appStoreId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
await openGooglePlay(appId: playStoreId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------------
|
||||
// Helper methods
|
||||
//----------------------------------------------------------------------------
|
||||
|
||||
static String? _appCountry;
|
||||
static String? _appBundle;
|
||||
static String? _appId;
|
||||
|
||||
/// It would be great if I could add country code into AppStore lookup URL.
|
||||
/// Eg: AppReview.setCountryCode('jp');
|
||||
static void setCountryCode(String code) =>
|
||||
_appCountry = code.isEmpty ? null : code;
|
||||
|
||||
/// Require app review for iOS
|
||||
static Future<void> _openIosReview({
|
||||
String? appId,
|
||||
bool compose = false,
|
||||
}) async {
|
||||
if (compose) {
|
||||
final id = appId ?? (await getIosAppId()) ?? '';
|
||||
try {
|
||||
await _api.openAppStoreReview(id);
|
||||
} catch (e) {
|
||||
throw AppReviewStoreListingFailedException(e.toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _api.requestReview(false);
|
||||
if (result != null && result.toLowerCase().contains("not available")) {
|
||||
throw AppReviewUnavailableException(result);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is AppReviewException) rethrow;
|
||||
throw AppReviewRequestFailedException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Require app review for Android
|
||||
static Future<void> _openAndroidReview(
|
||||
{String? appId, bool useAndroidTestMode = false}) async {
|
||||
try {
|
||||
final result = await _api.requestReview(useAndroidTestMode);
|
||||
if (result != null && result.toLowerCase().contains("not available")) {
|
||||
throw AppReviewUnavailableException(result);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is AppReviewException) rethrow;
|
||||
// If request fails, try opening store
|
||||
await openGooglePlay(appId: appId);
|
||||
}
|
||||
}
|
||||
|
||||
/// Open in AppStore
|
||||
static Future<void> openAppStore({
|
||||
String? fallbackUrl,
|
||||
String? appId,
|
||||
}) async {
|
||||
final id = appId ?? await getIosAppId() ?? '';
|
||||
try {
|
||||
await _api.openStoreListing(id);
|
||||
} catch (e) {
|
||||
throw AppReviewStoreListingFailedException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/// Open in GooglePlay
|
||||
static Future<void> openGooglePlay({
|
||||
String? fallbackUrl,
|
||||
String? appId,
|
||||
}) async {
|
||||
final bundle = appId ?? await getBundleName() ?? '';
|
||||
try {
|
||||
await _api.openStoreListing(bundle);
|
||||
} catch (e) {
|
||||
throw AppReviewStoreListingFailedException(e.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/// Base exception for AppReview package
|
||||
class AppReviewException implements Exception {
|
||||
final String message;
|
||||
|
||||
AppReviewException(this.message);
|
||||
|
||||
@override
|
||||
String toString() => 'AppReviewException: $message';
|
||||
}
|
||||
|
||||
/// Thrown when the review request fails
|
||||
class AppReviewRequestFailedException extends AppReviewException {
|
||||
AppReviewRequestFailedException(String message) : super(message);
|
||||
}
|
||||
|
||||
/// Thrown when the review is unavailable
|
||||
class AppReviewUnavailableException extends AppReviewException {
|
||||
AppReviewUnavailableException(String message) : super(message);
|
||||
}
|
||||
|
||||
/// Thrown when the store listing cannot be opened
|
||||
class AppReviewStoreListingFailedException extends AppReviewException {
|
||||
AppReviewStoreListingFailedException(String message) : super(message);
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import '../app_review.dart';
|
||||
|
||||
/// Get app bundle name
|
||||
Future<String?> getAppId() => AppReview.getBundleName();
|
||||
|
||||
/// Require app review for Android
|
||||
Future<String?> openAndroidReview() => AppReview.openAndroidReview();
|
||||
|
||||
/// Open in GooglePlay
|
||||
Future<String> openGooglePlay({String? fallbackUrl}) =>
|
||||
AppReview.openGooglePlay(fallbackUrl: fallbackUrl);
|
||||
@@ -1,17 +0,0 @@
|
||||
import '../app_review.dart';
|
||||
|
||||
/// Returns Apple ID for iOS application.
|
||||
///
|
||||
/// If there is no such application in App Store - returns empty string.
|
||||
Future<String?> getIosAppId(String appId, {String? countryCode}) async =>
|
||||
AppReview.getIosAppId(bundleId: appId, countryCode: countryCode);
|
||||
|
||||
/// Open store page with action write review.
|
||||
///
|
||||
/// Supported only for iOS, on Android [storeListing] will be executed.
|
||||
Future<String?> writeIosReview(String appId, {bool compose = false}) async =>
|
||||
AppReview.openIosReview(appId: appId, compose: compose);
|
||||
|
||||
/// Open in AppStore
|
||||
Future<String> openAppStore({String? fallbackUrl}) =>
|
||||
AppReview.openAppStore(fallbackUrl: fallbackUrl);
|
||||
@@ -0,0 +1,212 @@
|
||||
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
void writeValue(WriteBuffer buffer, Object? value) {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppReviewApi {
|
||||
/// Constructor for [AppReviewApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
AppReviewApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
Future<String?> requestReview(bool testMode) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.app_review.AppReviewApi.requestReview$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[testMode]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as String?);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if [requestReview] feature available.
|
||||
Future<bool> isRequestReviewAvailable() async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.app_review.AppReviewApi.isRequestReviewAvailable$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as bool?)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the store listing for the specified app.
|
||||
///
|
||||
/// [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
Future<void> openStoreListing(String? storeId) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.app_review.AppReviewApi.openStoreListing$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[storeId]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the App Store review page (iOS/macOS only).
|
||||
///
|
||||
/// [storeId] is the App ID.
|
||||
Future<void> openAppStoreReview(String? storeId) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.app_review.AppReviewApi.openAppStoreReview$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[storeId]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns package name for application.
|
||||
Future<String> getBundleId() async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.app_review.AppReviewApi.getBundleId$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else if (pigeonVar_replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as String?)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
///
|
||||
/// [bundleId] is the bundle identifier to look up.
|
||||
/// [countryCode] is the optional country code.
|
||||
Future<String?> lookupAppId(String bundleId, String? countryCode) async {
|
||||
final pigeonVar_channelName = 'dev.flutter.pigeon.app_review.AppReviewApi.lookupAppId$pigeonVar_messageChannelSuffix';
|
||||
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[bundleId, countryCode]);
|
||||
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return (pigeonVar_replyList[0] as String?);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
import '../app_review.dart';
|
||||
|
||||
/// Lazyload package info instance
|
||||
Future<PackageInfo?> getPackageInfo() async => AppReview.getPackageInfo();
|
||||
|
||||
/// Get package bundle name
|
||||
Future<String?> getPackageName() async => AppReview.getBundleName();
|
||||
@@ -0,0 +1,123 @@
|
||||
import Cocoa
|
||||
import FlutterMacOS
|
||||
import StoreKit
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public class AppReviewPlugin: NSObject, FlutterPlugin, AppReviewApi {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let instance = AppReviewPlugin()
|
||||
AppReviewApiSetup.setUp(binaryMessenger: registrar.messenger, api: instance)
|
||||
}
|
||||
|
||||
public func requestReview(testMode: Bool, completion: @escaping (Result<String?, Error>) -> Void) {
|
||||
if #available(macOS 14.0, *) {
|
||||
DispatchQueue.main.async {
|
||||
if let window = NSApplication.shared.windows.first {
|
||||
let view = NSHostingView(rootView: ReviewRequestView())
|
||||
view.frame = .zero
|
||||
window.contentView?.addSubview(view)
|
||||
// Cleanup? The view is zero frame and invisible, but triggers onAppear.
|
||||
// Ideally we remove it later, but for now this triggers the action.
|
||||
}
|
||||
}
|
||||
completion(.success("Requested Review via SwiftUI"))
|
||||
} else if #available(macOS 10.14, *) {
|
||||
SKStoreReviewController.requestReview()
|
||||
completion(.success("Requested Review"))
|
||||
} else {
|
||||
completion(.success("Review not available"))
|
||||
}
|
||||
}
|
||||
|
||||
public func isRequestReviewAvailable(completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||
if #available(macOS 10.14, *) {
|
||||
completion(.success(true))
|
||||
} else {
|
||||
completion(.success(false))
|
||||
}
|
||||
}
|
||||
|
||||
public func getBundleId(completion: @escaping (Result<String, Error>) -> Void) {
|
||||
completion(.success(Bundle.main.bundleIdentifier ?? ""))
|
||||
}
|
||||
|
||||
public func openStoreListing(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let storeId = storeId, !storeId.isEmpty else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 404, userInfo: [NSLocalizedDescriptionKey: "Store ID is missing"])))
|
||||
return
|
||||
}
|
||||
let urlString = "https://apps.apple.com/app/id\(storeId)"
|
||||
if let url = URL(string: urlString) {
|
||||
if NSWorkspace.shared.open(url) {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to open store listing"])))
|
||||
}
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid Store URL"])))
|
||||
}
|
||||
}
|
||||
|
||||
public func openAppStoreReview(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
guard let storeId = storeId, !storeId.isEmpty else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 404, userInfo: [NSLocalizedDescriptionKey: "Store ID is missing"])))
|
||||
return
|
||||
}
|
||||
let urlString = "https://apps.apple.com/app/id\(storeId)?action=write-review"
|
||||
if let url = URL(string: urlString) {
|
||||
if NSWorkspace.shared.open(url) {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 500, userInfo: [NSLocalizedDescriptionKey: "Failed to open App Store review"])))
|
||||
}
|
||||
} else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid Store URL"])))
|
||||
}
|
||||
}
|
||||
|
||||
public func lookupAppId(bundleId: String, countryCode: String?, completion: @escaping (Result<String?, Error>) -> Void) {
|
||||
let country = countryCode ?? ""
|
||||
let urlString = "https://itunes.apple.com/\(country)/lookup?bundleId=\(bundleId)"
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(.failure(NSError(domain: "AppReview", code: 400, userInfo: [NSLocalizedDescriptionKey: "Invalid Lookup URL"])))
|
||||
return
|
||||
}
|
||||
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
guard let data = data else {
|
||||
completion(.success(nil))
|
||||
return
|
||||
}
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let results = json["results"] as? [[String: Any]],
|
||||
let firstResult = results.first,
|
||||
let trackId = firstResult["trackId"] as? Int {
|
||||
completion(.success(String(trackId)))
|
||||
} else {
|
||||
completion(.success(nil))
|
||||
}
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
struct ReviewRequestView: View {
|
||||
@Environment(\.requestReview) var requestReview
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
.onAppear {
|
||||
requestReview()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Autogenerated from Pigeon (v26.1.5), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
|
||||
import Foundation
|
||||
|
||||
#if os(iOS)
|
||||
import Flutter
|
||||
#elseif os(macOS)
|
||||
import FlutterMacOS
|
||||
#else
|
||||
#error("Unsupported platform.")
|
||||
#endif
|
||||
|
||||
/// Error class for passing custom error details to Dart side.
|
||||
final class PigeonError: Error {
|
||||
let code: String
|
||||
let message: String?
|
||||
let details: Sendable?
|
||||
|
||||
init(code: String, message: String?, details: Sendable?) {
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.details = details
|
||||
}
|
||||
|
||||
var localizedDescription: String {
|
||||
return
|
||||
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
|
||||
}
|
||||
}
|
||||
|
||||
private func wrapResult(_ result: Any?) -> [Any?] {
|
||||
return [result]
|
||||
}
|
||||
|
||||
private func wrapError(_ error: Any) -> [Any?] {
|
||||
if let pigeonError = error as? PigeonError {
|
||||
return [
|
||||
pigeonError.code,
|
||||
pigeonError.message,
|
||||
pigeonError.details,
|
||||
]
|
||||
}
|
||||
if let flutterError = error as? FlutterError {
|
||||
return [
|
||||
flutterError.code,
|
||||
flutterError.message,
|
||||
flutterError.details,
|
||||
]
|
||||
}
|
||||
return [
|
||||
"\(error)",
|
||||
"\(type(of: error))",
|
||||
"Stacktrace: \(Thread.callStackSymbols)",
|
||||
]
|
||||
}
|
||||
|
||||
private func isNullish(_ value: Any?) -> Bool {
|
||||
return value is NSNull || value == nil
|
||||
}
|
||||
|
||||
private func nilOrValue<T>(_ value: Any?) -> T? {
|
||||
if value is NSNull { return nil }
|
||||
return value as! T?
|
||||
}
|
||||
|
||||
|
||||
private class MessagesPigeonCodecReader: FlutterStandardReader {
|
||||
}
|
||||
|
||||
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
|
||||
}
|
||||
|
||||
private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
|
||||
override func reader(with data: Data) -> FlutterStandardReader {
|
||||
return MessagesPigeonCodecReader(data: data)
|
||||
}
|
||||
|
||||
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
|
||||
return MessagesPigeonCodecWriter(data: data)
|
||||
}
|
||||
}
|
||||
|
||||
class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
|
||||
static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
|
||||
}
|
||||
|
||||
|
||||
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
|
||||
protocol AppReviewApi {
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
func requestReview(testMode: Bool, completion: @escaping (Result<String?, Error>) -> Void)
|
||||
/// Check if [requestReview] feature available.
|
||||
func isRequestReviewAvailable(completion: @escaping (Result<Bool, Error>) -> Void)
|
||||
/// Opens the store listing for the specified app.
|
||||
///
|
||||
/// [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
func openStoreListing(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
/// Opens the App Store review page (iOS/macOS only).
|
||||
///
|
||||
/// [storeId] is the App ID.
|
||||
func openAppStoreReview(storeId: String?, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
/// Returns package name for application.
|
||||
func getBundleId(completion: @escaping (Result<String, Error>) -> Void)
|
||||
/// Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
///
|
||||
/// [bundleId] is the bundle identifier to look up.
|
||||
/// [countryCode] is the optional country code.
|
||||
func lookupAppId(bundleId: String, countryCode: String?, completion: @escaping (Result<String?, Error>) -> Void)
|
||||
}
|
||||
|
||||
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
|
||||
class AppReviewApiSetup {
|
||||
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
|
||||
/// Sets up an instance of `AppReviewApi` to handle messages through the `binaryMessenger`.
|
||||
static func setUp(binaryMessenger: FlutterBinaryMessenger, api: AppReviewApi?, messageChannelSuffix: String = "") {
|
||||
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
let requestReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.requestReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
requestReviewChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let testModeArg = args[0] as! Bool
|
||||
api.requestReview(testMode: testModeArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
requestReviewChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Check if [requestReview] feature available.
|
||||
let isRequestReviewAvailableChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.isRequestReviewAvailable\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
isRequestReviewAvailableChannel.setMessageHandler { _, reply in
|
||||
api.isRequestReviewAvailable { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
isRequestReviewAvailableChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Opens the store listing for the specified app.
|
||||
///
|
||||
/// [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
let openStoreListingChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.openStoreListing\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
openStoreListingChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let storeIdArg: String? = nilOrValue(args[0])
|
||||
api.openStoreListing(storeId: storeIdArg) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openStoreListingChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Opens the App Store review page (iOS/macOS only).
|
||||
///
|
||||
/// [storeId] is the App ID.
|
||||
let openAppStoreReviewChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.openAppStoreReview\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
openAppStoreReviewChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let storeIdArg: String? = nilOrValue(args[0])
|
||||
api.openAppStoreReview(storeId: storeIdArg) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
reply(wrapResult(nil))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openAppStoreReviewChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Returns package name for application.
|
||||
let getBundleIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.getBundleId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
getBundleIdChannel.setMessageHandler { _, reply in
|
||||
api.getBundleId { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
getBundleIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
/// Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
///
|
||||
/// [bundleId] is the bundle identifier to look up.
|
||||
/// [countryCode] is the optional country code.
|
||||
let lookupAppIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.app_review.AppReviewApi.lookupAppId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
|
||||
if let api = api {
|
||||
lookupAppIdChannel.setMessageHandler { message, reply in
|
||||
let args = message as! [Any?]
|
||||
let bundleIdArg = args[0] as! String
|
||||
let countryCodeArg: String? = nilOrValue(args[1])
|
||||
api.lookupAppId(bundleId: bundleIdArg, countryCode: countryCodeArg) { result in
|
||||
switch result {
|
||||
case .success(let res):
|
||||
reply(wrapResult(res))
|
||||
case .failure(let error):
|
||||
reply(wrapError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lookupAppIdChannel.setMessageHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#
|
||||
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
|
||||
# Run `pod lib lint app_review.podspec` to validate before publishing.
|
||||
#
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'app_review'
|
||||
s.version = '0.0.1'
|
||||
s.summary = 'Request and Write Reviews and Open Store Listing for Android/iOS/macOS in Flutter.'
|
||||
s.description = <<-DESC
|
||||
Request and Write Reviews and Open Store Listing for Android/iOS/macOS in Flutter.
|
||||
DESC
|
||||
s.homepage = 'http://example.com'
|
||||
s.license = { :file => '../LICENSE' }
|
||||
s.author = { 'Your Company' => 'email@example.com' }
|
||||
s.source = { :path => '.' }
|
||||
s.source_files = 'Classes/**/*'
|
||||
s.dependency 'FlutterMacOS'
|
||||
|
||||
s.platform = :osx, '10.14'
|
||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||
s.swift_version = '5.0'
|
||||
end
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:pigeon/pigeon.dart';
|
||||
|
||||
@ConfigurePigeon(PigeonOptions(
|
||||
dartOut: 'lib/src/messages.g.dart',
|
||||
dartOptions: DartOptions(),
|
||||
kotlinOut: 'android/src/main/java/com/appleeducate/appreview/Messages.g.kt',
|
||||
kotlinOptions: KotlinOptions(package: 'com.appleeducate.appreview'),
|
||||
javaOut: 'android/src/main/java/com/appleeducate/appreview/Messages.java',
|
||||
javaOptions: JavaOptions(package: 'com.appleeducate.appreview'),
|
||||
swiftOut: 'ios/Classes/Messages.g.swift',
|
||||
swiftOptions: SwiftOptions(),
|
||||
))
|
||||
@HostApi()
|
||||
abstract class AppReviewApi {
|
||||
/// Request review.
|
||||
///
|
||||
/// Tells StoreKit / Play Store to ask the user to rate or review your app, if appropriate.
|
||||
/// Supported only in iOS 10.3+ and Android with Play Services installed (see [isRequestReviewAvailable]).
|
||||
///
|
||||
/// Returns string with details message.
|
||||
@async
|
||||
String? requestReview(bool testMode);
|
||||
|
||||
/// Check if [requestReview] feature available.
|
||||
@async
|
||||
bool isRequestReviewAvailable();
|
||||
|
||||
/// Opens the store listing for the specified app.
|
||||
///
|
||||
/// [storeId] is the package name (Android) or App ID (iOS/macOS).
|
||||
@async
|
||||
void openStoreListing(String? storeId);
|
||||
|
||||
/// Opens the App Store review page (iOS/macOS only).
|
||||
///
|
||||
/// [storeId] is the App ID.
|
||||
@async
|
||||
void openAppStoreReview(String? storeId);
|
||||
|
||||
/// Returns package name for application.
|
||||
@async
|
||||
String getBundleId();
|
||||
|
||||
/// Requests the App ID from the App Store (via iTunes Lookup API).
|
||||
///
|
||||
/// [bundleId] is the bundle identifier to look up.
|
||||
/// [countryCode] is the optional country code.
|
||||
@async
|
||||
String? lookupAppId(String bundleId, String? countryCode);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
name: app_review
|
||||
description: Request and Write Reviews and Open Store Listing for Android and iOS in Flutter.
|
||||
version: 2.2.0
|
||||
version: 3.0.0
|
||||
environment:
|
||||
sdk: ^3.5.0
|
||||
flutter: ^3.38.5
|
||||
@@ -9,13 +9,7 @@ maintainer: Rody Davis (@rodydavis)
|
||||
homepage: https://github.com/rodydavis/packages.dart
|
||||
repository: https://github.com/rodydavis/packages.dart
|
||||
|
||||
|
||||
dependencies:
|
||||
http: ^1.2.0
|
||||
|
||||
url_launcher: ^6.3.0
|
||||
package_info_plus: ^9.0.0
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
@@ -27,3 +21,8 @@ flutter:
|
||||
pluginClass: AppReviewPlugin
|
||||
ios:
|
||||
pluginClass: AppReviewPlugin
|
||||
macos:
|
||||
pluginClass: AppReviewPlugin
|
||||
dev_dependencies:
|
||||
pigeon: ^26.1.5
|
||||
|
||||
|
||||
@@ -192,22 +192,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -280,22 +264,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -573,14 +541,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user