Skip to content

Commit ecc74b6

Browse files
authored
Added experimental get_app_review_url() method (#4)
* Added iOS and godot implementations of experimental get_app_review_url() * Added Android implementation of get_app_review_url()
1 parent f69fc1a commit ecc74b6

File tree

10 files changed

+227
-29
lines changed

10 files changed

+227
-29
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ local.properties
2727
*.d
2828
*.o
2929
*.dblite
30+
xcuserdata/
3031
Pods/
3132
ios/godot/
3233

addon/InappReview.gd

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ signal review_info_generated()
1111
signal review_info_generation_failed()
1212
signal review_flow_launched()
1313
signal review_flow_launch_failed()
14+
signal app_review_url_ready(url: String)
15+
signal get_app_review_url_failed()
1416

1517
const SIGNAL_NAME_REVIEW_INFO_GENERATED: String = "review_info_generated";
1618
const SIGNAL_NAME_REVIEW_INFO_GENERATION_FAILED: String = "review_info_generation_failed";
1719
const SIGNAL_NAME_REVIEW_FLOW_LAUNCHED: String = "review_flow_launched";
1820
const SIGNAL_NAME_REVIEW_FLOW_LAUNCH_FAILED: String = "review_flow_launch_failed";
21+
const SIGNAL_NAME_APP_REVIEW_URL_READY: String = "app_review_url_ready";
22+
const SIGNAL_NAME_GET_APP_REVIEW_URL_FAILED: String = "get_app_review_url_failed";
1923

2024
var _plugin_singleton: Object
2125

@@ -43,6 +47,8 @@ func _connect_signals() -> void:
4347
_plugin_singleton.connect(SIGNAL_NAME_REVIEW_INFO_GENERATION_FAILED, _on_review_info_generation_failed)
4448
_plugin_singleton.connect(SIGNAL_NAME_REVIEW_FLOW_LAUNCHED, _on_review_flow_launched)
4549
_plugin_singleton.connect(SIGNAL_NAME_REVIEW_FLOW_LAUNCH_FAILED, _on_review_flow_launch_failed)
50+
_plugin_singleton.connect(SIGNAL_NAME_APP_REVIEW_URL_READY, _on_app_review_url_ready)
51+
_plugin_singleton.connect(SIGNAL_NAME_GET_APP_REVIEW_URL_FAILED, _on_get_app_review_url_failed)
4652

4753

4854
func generate_review_info() -> void:
@@ -59,6 +65,13 @@ func launch_review_flow() -> void:
5965
log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME)
6066

6167

68+
func get_app_review_url() -> void:
69+
if _plugin_singleton != null:
70+
_plugin_singleton.get_app_review_url()
71+
else:
72+
log_error("%s plugin not initialized" % PLUGIN_SINGLETON_NAME)
73+
74+
6275
func _on_review_info_generated() -> void:
6376
emit_signal(SIGNAL_NAME_REVIEW_INFO_GENERATED)
6477

@@ -75,6 +88,14 @@ func _on_review_flow_launch_failed() -> void:
7588
emit_signal(SIGNAL_NAME_REVIEW_FLOW_LAUNCH_FAILED)
7689

7790

91+
func _on_app_review_url_ready(a_url: String) -> void:
92+
emit_signal(SIGNAL_NAME_APP_REVIEW_URL_READY, a_url)
93+
94+
95+
func _on_get_app_review_url_failed() -> void:
96+
emit_signal(SIGNAL_NAME_GET_APP_REVIEW_URL_FAILED)
97+
98+
7899
static func log_error(a_description: String) -> void:
79100
push_error(a_description)
80101

android/inappreview/src/main/java/org/godotengine/plugin/android/inappreview/InappReviewPlugin.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class InappReviewPlugin extends GodotPlugin {
3737
private static final String SIGNAL_NAME_REVIEW_INFO_GENERATION_FAILED = "review_info_generation_failed";
3838
private static final String SIGNAL_NAME_REVIEW_FLOW_LAUNCHED = "review_flow_launched";
3939
private static final String SIGNAL_NAME_REVIEW_FLOW_LAUNCH_FAILED = "review_flow_launch_failed";
40+
private static final String SIGNAL_NAME_APP_REVIEW_URL_READY = "app_review_url_ready";
41+
private static final String SIGNAL_NAME_GET_APP_REVIEW_URL_FAILED = "get_app_review_url_failed";
4042

4143
private Activity activity;
4244
private ReviewManager manager;
@@ -93,6 +95,17 @@ public void launch_review_flow() {
9395
}
9496
}
9597

98+
@UsedByGodot
99+
public void get_app_review_url() {
100+
try {
101+
String packageName = activity.getApplicationContext().getPackageName();
102+
emitSignal(SIGNAL_NAME_APP_REVIEW_URL_READY, "https://play.google.com/store/apps/details?id=" + packageName);
103+
} catch (Exception e) {
104+
Log.e(LOG_TAG, "get_app_review_url() failed due to " + e.getMessage());
105+
emitSignal(SIGNAL_NAME_GET_APP_REVIEW_URL_FAILED);
106+
}
107+
}
108+
96109
@NonNull
97110
@Override
98111
public String getPluginName() {
@@ -108,6 +121,8 @@ public Set<SignalInfo> getPluginSignals() {
108121
signals.add(new SignalInfo(SIGNAL_NAME_REVIEW_INFO_GENERATION_FAILED));
109122
signals.add(new SignalInfo(SIGNAL_NAME_REVIEW_FLOW_LAUNCHED));
110123
signals.add(new SignalInfo(SIGNAL_NAME_REVIEW_FLOW_LAUNCH_FAILED));
124+
signals.add(new SignalInfo(SIGNAL_NAME_APP_REVIEW_URL_READY, String.class));
125+
signals.add(new SignalInfo(SIGNAL_NAME_GET_APP_REVIEW_URL_FAILED));
111126

112127
return signals;
113128
}

demo/Main.gd

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,14 @@ func _ready() -> void:
2222
_active_texture_rect = _android_texture_rect
2323

2424

25-
func _on_button_pressed() -> void:
25+
func _on_launch_review_button_pressed() -> void:
2626
inapp_review.generate_review_info()
2727

2828

29+
func _on_get_review_url_button_pressed() -> void:
30+
inapp_review.get_app_review_url()
31+
32+
2933
func _on_inapp_review_review_info_generated() -> void:
3034
_print_to_screen("In-app review info generation success!")
3135
_print_to_screen("Launching review flow...")
@@ -44,6 +48,14 @@ func _on_inapp_review_review_flow_launch_failed() -> void:
4448
_print_to_screen("In-app review flow launch failed!", true)
4549

4650

51+
func _on_inapp_review_app_review_url_ready(a_url: String) -> void:
52+
_print_to_screen("App review URL: %s" % a_url)
53+
54+
55+
func _on_inapp_review_get_app_review_url_failed() -> void:
56+
_print_to_screen("Failed to get app review URL", true)
57+
58+
4759
func _print_to_screen(a_message: String, a_is_error: bool = false) -> void:
4860
_label.add_text("%s\n\n" % a_message)
4961
if a_is_error:

demo/Main.tscn

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,28 @@ horizontal_alignment = 1
4343

4444
[node name="VBoxContainer" type="VBoxContainer" parent="CanvasLayer/CenterContainer/VBoxContainer"]
4545
layout_mode = 2
46-
theme_override_constants/separation = 10
46+
theme_override_constants/separation = 20
4747

48-
[node name="Button" type="Button" parent="CanvasLayer/CenterContainer/VBoxContainer/VBoxContainer"]
48+
[node name="LaunchReviewButton" type="Button" parent="CanvasLayer/CenterContainer/VBoxContainer/VBoxContainer"]
4949
custom_minimum_size = Vector2(375, 0)
5050
layout_mode = 2
5151
text = "Launch Review"
5252

53+
[node name="GetReviewUrlButton" type="Button" parent="CanvasLayer/CenterContainer/VBoxContainer/VBoxContainer"]
54+
layout_mode = 2
55+
text = "Get Review URL"
56+
5357
[node name="RichTextLabel" type="RichTextLabel" parent="CanvasLayer/CenterContainer/VBoxContainer"]
5458
custom_minimum_size = Vector2(250, 300)
5559
layout_mode = 2
5660

5761
[node name="InappReview" type="Node" parent="."]
5862
script = ExtResource("2_0jlfk")
5963

60-
[connection signal="pressed" from="CanvasLayer/CenterContainer/VBoxContainer/VBoxContainer/Button" to="." method="_on_button_pressed"]
64+
[connection signal="pressed" from="CanvasLayer/CenterContainer/VBoxContainer/VBoxContainer/LaunchReviewButton" to="." method="_on_launch_review_button_pressed"]
65+
[connection signal="pressed" from="CanvasLayer/CenterContainer/VBoxContainer/VBoxContainer/GetReviewUrlButton" to="." method="_on_get_review_url_button_pressed"]
66+
[connection signal="app_review_url_ready" from="InappReview" to="." method="_on_inapp_review_app_review_url_ready"]
67+
[connection signal="get_app_review_url_failed" from="InappReview" to="." method="_on_inapp_review_get_app_review_url_failed"]
6168
[connection signal="review_flow_launch_failed" from="InappReview" to="." method="_on_inapp_review_review_flow_launch_failed"]
6269
[connection signal="review_flow_launched" from="InappReview" to="." method="_on_inapp_review_review_flow_launched"]
6370
[connection signal="review_info_generated" from="InappReview" to="." method="_on_inapp_review_review_info_generated"]

ios/inapp_review/InappReview.swift

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// © 2024-present https://github.com/cengiz-pz
3+
//
4+
5+
import Foundation
6+
import StoreKit
7+
import OSLog
8+
9+
@available(iOS 16.0, *)
10+
@objcMembers public class InappReview : NSObject
11+
{
12+
13+
private static let logger = Logger(subsystem: "org.godotengine.plugin", category: "InappReview")
14+
15+
static func requestReview() {
16+
logger.debug("Attempting to request in-app review.")
17+
18+
// Dispatch to main thread (Godot may call us from background)
19+
DispatchQueue.main.async {
20+
// Find the active foreground scene
21+
guard let scene = UIApplication.shared.connectedScenes
22+
.compactMap({ $0 as? UIWindowScene })
23+
.first(where: { $0.activationState == .foregroundActive }) else {
24+
25+
logger.error("Failed to find an active foreground UIWindowScene. Cannot request review.")
26+
return
27+
}
28+
29+
if #available(iOS 18.0, *) {
30+
logger.debug("Using AppStore.requestReview(in:).")
31+
AppStore.requestReview(in: scene)
32+
} else if #available(iOS 14.0, *) {
33+
logger.debug("Using SKStoreReviewController.requestReview(in:).")
34+
SKStoreReviewController.requestReview(in: scene)
35+
} else {
36+
logger.debug("Using SKStoreReviewController.requestReview().")
37+
SKStoreReviewController.requestReview()
38+
}
39+
40+
logger.info("requestReview method called successfully.")
41+
}
42+
}
43+
44+
func fetchAppStoreID(completion: @escaping (String?) -> Void) {
45+
Self.logger.debug("Starting fetchAppStoreID process.")
46+
47+
// Get the local Bundle ID
48+
guard let bundleId = Bundle.main.bundleIdentifier,
49+
let encodedBundleId = bundleId.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
50+
Self.logger.error("Failed to get or encode bundle identifier.")
51+
completion(nil)
52+
return
53+
}
54+
55+
Self.logger.debug("Bundle ID: \(bundleId, privacy: .public)")
56+
57+
// Construct the iTunes Lookup URL
58+
let lookupURLString = "https://itunes.apple.com/lookup?bundleId=\(encodedBundleId)"
59+
guard let lookupURL = URL(string: lookupURLString) else {
60+
Self.logger.error("Failed to construct valid lookup URL from string: \(lookupURLString, privacy: .public)")
61+
completion(nil)
62+
return
63+
}
64+
65+
Self.logger.debug("Lookup URL: \(lookupURL.absoluteString, privacy: .public)")
66+
67+
// Perform the network request
68+
URLSession.shared.dataTask(with: lookupURL) { data, response, error in
69+
70+
// Check for network errors
71+
if let error = error {
72+
Self.logger.error("Network request failed: \(error.localizedDescription, privacy: .public)")
73+
completion(nil)
74+
return
75+
}
76+
77+
// Check for data presence
78+
guard let data = data else {
79+
Self.logger.error("Network request returned no data and no error.")
80+
completion(nil)
81+
return
82+
}
83+
84+
do {
85+
// Parse the JSON response
86+
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
87+
let results = json["results"] as? [[String: Any]],
88+
let firstResult = results.first,
89+
let trackId = firstResult["trackId"] as? Int {
90+
91+
let storeId = String(trackId) // The trackId is the App Store ID
92+
93+
Self.logger.info("Successfully fetched App Store ID: \(storeId, privacy: .public)")
94+
completion(storeId)
95+
} else {
96+
Self.logger.error("JSON parsing failed to extract 'trackId' (App Store ID).")
97+
completion(nil)
98+
}
99+
} catch {
100+
Self.logger.error("JSON serialization error: \(error.localizedDescription, privacy: .public)")
101+
completion(nil)
102+
}
103+
}.resume()
104+
}
105+
106+
/**
107+
* Asynchronously fetches the application's Store ID and constructs the URL
108+
* that directs the user directly to the 'Write a Review' page in the App Store.
109+
*
110+
* @param completion A closure that receives the constructed review URL or nil on failure.
111+
*/
112+
func getAppReviewUrl(completion: @escaping (URL?) -> Void) {
113+
Self.logger.debug("Starting getAppReviewUrl process.")
114+
115+
fetchAppStoreID { storeId in
116+
guard let storeId = storeId,
117+
let url = URL(string: "https://apps.apple.com/app/id\(storeId)?action=write-review") else {
118+
119+
Self.logger.error("Failed to get App Store ID or construct review URL.")
120+
completion(nil)
121+
return
122+
}
123+
124+
Self.logger.info("Successfully constructed review URL: \(url.absoluteString, privacy: .public)")
125+
completion(url)
126+
}
127+
}
128+
}

ios/inapp_review/SwiftClass.swift

Lines changed: 0 additions & 13 deletions
This file was deleted.

ios/inapp_review/inapp_review_plugin.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class InappReviewPlugin : public Object {
2020
public:
2121
void generate_review_info();
2222
void launch_review_flow();
23+
void get_app_review_url();
2324

2425
InappReviewPlugin();
2526
~InappReviewPlugin();
Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//
22
// © 2024-present https://github.com/cengiz-pz
33
//
4+
45
#import <Foundation/Foundation.h>
56

67
#include "core/config/project_settings.h"
@@ -13,39 +14,64 @@
1314
String const REVIEW_INFO_GENERATION_FAILED_SIGNAL = "review_info_generation_failed";
1415
String const REVIEW_FLOW_LAUNCHED_SIGNAL = "review_flow_launched";
1516
String const REVIEW_FLOW_LAUNCH_FAILED_SIGNAL = "review_flow_launch_failed";
17+
String const APP_REVIEW_URL_READY_SIGNAL = "app_review_url_ready";
18+
String const GET_APP_REVIEW_URL_FAILED_SIGNAL = "get_app_review_url_failed";
19+
20+
static NSString *const kLogTag = @"InappReviewPlugin::";
1621

1722

18-
/*
19-
* Bind plugin's public interface
20-
*/
2123
void InappReviewPlugin::_bind_methods() {
2224
ClassDB::bind_method(D_METHOD("generate_review_info"), &InappReviewPlugin::generate_review_info);
2325
ClassDB::bind_method(D_METHOD("launch_review_flow"), &InappReviewPlugin::launch_review_flow);
26+
ClassDB::bind_method(D_METHOD("get_app_review_url"), &InappReviewPlugin::get_app_review_url);
2427

2528
ADD_SIGNAL(MethodInfo(REVIEW_INFO_GENERATED_SIGNAL));
2629
ADD_SIGNAL(MethodInfo(REVIEW_INFO_GENERATION_FAILED_SIGNAL));
2730
ADD_SIGNAL(MethodInfo(REVIEW_FLOW_LAUNCHED_SIGNAL));
2831
ADD_SIGNAL(MethodInfo(REVIEW_FLOW_LAUNCH_FAILED_SIGNAL));
32+
ADD_SIGNAL(MethodInfo(APP_REVIEW_URL_READY_SIGNAL, PropertyInfo(Variant::STRING, "url")));
33+
ADD_SIGNAL(MethodInfo(GET_APP_REVIEW_URL_FAILED_SIGNAL));
2934
}
3035

3136
// Only for platform parity.
3237
void InappReviewPlugin::generate_review_info() {
33-
NSLog(@"InappReviewPlugin generate_review_info");
38+
NSLog(@"%@ generate_review_info", kLogTag);
3439

3540
emit_signal(REVIEW_INFO_GENERATED_SIGNAL);
3641
}
3742

3843
void InappReviewPlugin::launch_review_flow() {
39-
NSLog(@"InappReviewPlugin launch_review_flow");
40-
[SwiftClass launch_review_flow];
44+
NSLog(@"%@ launch_review_flow", kLogTag);
45+
[InappReview requestReview];
4146

4247
emit_signal(REVIEW_FLOW_LAUNCHED_SIGNAL);
4348
}
4449

50+
void InappReviewPlugin::get_app_review_url() {
51+
NSLog(@"%@ get_app_review_url", kLogTag);
52+
53+
InappReviewPlugin *plugin_instance = this;
54+
55+
InappReview *swiftInstance = [[InappReview alloc] init];
56+
[swiftInstance getAppReviewUrlWithCompletion:^(NSURL * _Nullable url) {
57+
if (url) {
58+
NSLog(@"%@ App review URL successfully constructed: %@", kLogTag, url);
59+
plugin_instance->call_deferred(
60+
"emit_signal",
61+
REVIEW_INFO_GENERATED_SIGNAL,
62+
String(url.absoluteString.UTF8String)
63+
);
64+
} else {
65+
NSLog(@"%@ Error: Failed to construct app review URL", kLogTag);
66+
plugin_instance->call_deferred("emit_signal", GET_APP_REVIEW_URL_FAILED_SIGNAL);
67+
}
68+
}];
69+
}
70+
4571
InappReviewPlugin::InappReviewPlugin() {
46-
NSLog(@"InappReviewPlugin constructor");
72+
NSLog(@"%@ constructor", kLogTag);
4773
}
4874

4975
InappReviewPlugin::~InappReviewPlugin() {
50-
NSLog(@"InappReviewPlugin destructor");
76+
NSLog(@"%@ destructor", kLogTag);
5177
}

0 commit comments

Comments
 (0)