@symbiote-native/store-review 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +174 -0
- package/build/angular/index.d.ts +1 -0
- package/build/angular/index.js +4 -0
- package/build/core/index.d.ts +2 -0
- package/build/core/index.js +1 -0
- package/build/core/native-module.d.ts +5 -0
- package/build/core/native-module.js +3 -0
- package/build/core/store-review.d.ts +30 -0
- package/build/core/store-review.js +68 -0
- package/build/react/index.d.ts +1 -0
- package/build/react/index.js +5 -0
- package/build/vue/index.d.ts +1 -0
- package/build/vue/index.js +4 -0
- package/build-ngc/angular/index.d.ts +1 -0
- package/build-ngc/angular/index.js +5 -0
- package/build-ngc/angular/index.js.map +1 -0
- package/build-ngc/core/index.d.ts +2 -0
- package/build-ngc/core/index.js +2 -0
- package/build-ngc/core/index.js.map +1 -0
- package/build-ngc/core/native-module.d.ts +5 -0
- package/build-ngc/core/native-module.js +4 -0
- package/build-ngc/core/native-module.js.map +1 -0
- package/build-ngc/core/store-review.d.ts +30 -0
- package/build-ngc/core/store-review.js +69 -0
- package/build-ngc/core/store-review.js.map +1 -0
- package/native-link.json +12 -0
- package/package.json +107 -0
- package/src/angular/index.ts +4 -0
- package/src/core/index.ts +2 -0
- package/src/core/native-module.ts +16 -0
- package/src/core/store-review.test.ts +150 -0
- package/src/core/store-review.ts +78 -0
- package/src/react/index.ts +5 -0
- package/src/vue/index.ts +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 A. Prokopenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# @symbiote-native/store-review
|
|
2
|
+
|
|
3
|
+
A wrapper package for [SymbioteNative](../../README.md) that makes
|
|
4
|
+
[`expo-store-review`](https://github.com/expo/expo/tree/main/packages/expo-store-review)
|
|
5
|
+
— prompting the platform's native in-app review flow — usable from **every** adapter, React,
|
|
6
|
+
Vue, and Angular, not just React. Like [`@symbiote-native/device`](../device) and
|
|
7
|
+
[`@symbiote-native/local-auth`](../local-auth), every export here is a one-shot async call with
|
|
8
|
+
no per-instance state, so there is no hook/composable/service to wrap — the React, Vue, and
|
|
9
|
+
Angular entry points are plain re-exports of the same `core`.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @symbiote-native/store-review
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`expo-store-review` and `expo-modules-core` come along as regular dependencies, pinned to exact
|
|
18
|
+
versions — never install either yourself, and never add the `expo` meta-package to your project
|
|
19
|
+
(it bundles its own Metro/Babel pipeline, which conflicts with this project's own).
|
|
20
|
+
|
|
21
|
+
## Required one-time step: native autolinking wiring
|
|
22
|
+
|
|
23
|
+
Unlike a plain RN native module, `expo-store-review`'s native code is discovered by
|
|
24
|
+
`expo-modules-autolinking`, not RN's own `react-native.config.cjs` mechanism — this needs wiring
|
|
25
|
+
into the native host app **once**, covering this package and every other `expo-modules-core`
|
|
26
|
+
package with zero further changes:
|
|
27
|
+
|
|
28
|
+
| Platform | Touches |
|
|
29
|
+
|---|---|
|
|
30
|
+
| iOS | `ios/Podfile` — add `use_expo_modules!` |
|
|
31
|
+
| iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
|
|
32
|
+
| Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
|
|
33
|
+
| Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
|
|
34
|
+
|
|
35
|
+
Full mechanics live in the `symbiote-expo-native-module` project skill. Reference
|
|
36
|
+
implementation: `examples/expo-react/ios/Podfile` and
|
|
37
|
+
`examples/expo-react/android/app/src/main/java/com/canaryexpo/MainApplication.kt`.
|
|
38
|
+
|
|
39
|
+
`expo-store-review` needs no runtime permission on either platform.
|
|
40
|
+
|
|
41
|
+
## Shape
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
src/core/ isAvailableAsync / requestReview / hasAction, plus the IStoreReviewUrlOptions
|
|
45
|
+
type. native-module.ts resolves the native module via expo-modules-core's
|
|
46
|
+
requireNativeModule.
|
|
47
|
+
src/react/ @symbiote-native/store-review/react — export * from '../core'
|
|
48
|
+
src/vue/ @symbiote-native/store-review/vue — export * from '../core'
|
|
49
|
+
src/angular/ @symbiote-native/store-review/angular — export * from '../core'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
No per-adapter lifecycle wrapper exists because there's nothing to subscribe to or clean up —
|
|
53
|
+
each adapter entry is a single-file re-export.
|
|
54
|
+
|
|
55
|
+
## Use it
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
// React
|
|
59
|
+
import { Button } from '@symbiote-native/react';
|
|
60
|
+
import { requestReview } from '@symbiote-native/store-review/react';
|
|
61
|
+
|
|
62
|
+
function RateAppButton() {
|
|
63
|
+
return (
|
|
64
|
+
<Button
|
|
65
|
+
title="Rate this app"
|
|
66
|
+
onPress={() =>
|
|
67
|
+
requestReview({
|
|
68
|
+
iosAppStoreUrl: 'https://apps.apple.com/app/id123456789',
|
|
69
|
+
androidPlayStoreUrl: 'https://play.google.com/store/apps/details?id=com.example.app',
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
/>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<!-- Vue -->
|
|
79
|
+
<script setup lang="ts">
|
|
80
|
+
import { Button } from '@symbiote-native/vue';
|
|
81
|
+
import { requestReview } from '@symbiote-native/store-review/vue';
|
|
82
|
+
|
|
83
|
+
function onRatePress() {
|
|
84
|
+
void requestReview({
|
|
85
|
+
iosAppStoreUrl: 'https://apps.apple.com/app/id123456789',
|
|
86
|
+
androidPlayStoreUrl: 'https://play.google.com/store/apps/details?id=com.example.app',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<Button title="Rate this app" @press="onRatePress" />
|
|
93
|
+
</template>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// Angular
|
|
98
|
+
import { Component } from '@angular/core';
|
|
99
|
+
import { Button } from '@symbiote-native/angular';
|
|
100
|
+
import { requestReview } from '@symbiote-native/store-review/angular';
|
|
101
|
+
|
|
102
|
+
@Component({
|
|
103
|
+
standalone: true,
|
|
104
|
+
imports: [Button],
|
|
105
|
+
template: `<Button title="Rate this app" (press)="onRatePress()" />`,
|
|
106
|
+
})
|
|
107
|
+
export class RateAppButton {
|
|
108
|
+
onRatePress(): void {
|
|
109
|
+
void requestReview({
|
|
110
|
+
iosAppStoreUrl: 'https://apps.apple.com/app/id123456789',
|
|
111
|
+
androidPlayStoreUrl: 'https://play.google.com/store/apps/details?id=com.example.app',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
These snippets are not yet demoed in a real canary screen — treat them as realistic reference
|
|
118
|
+
code, not a trimmed-down copy of a running demo.
|
|
119
|
+
|
|
120
|
+
## API
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
export type IStoreReviewUrlOptions = {
|
|
124
|
+
iosAppStoreUrl?: string;
|
|
125
|
+
androidPlayStoreUrl?: string;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
isAvailableAsync(): Promise<boolean>
|
|
129
|
+
requestReview(options?: IStoreReviewUrlOptions): Promise<void>
|
|
130
|
+
hasAction(options?: IStoreReviewUrlOptions): Promise<boolean>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { requestReview, isAvailableAsync, hasAction } from '@symbiote-native/store-review';
|
|
135
|
+
// or the framework-scoped entry points — identical surface, re-exported verbatim:
|
|
136
|
+
import { requestReview } from '@symbiote-native/store-review/react';
|
|
137
|
+
import { requestReview } from '@symbiote-native/store-review/vue';
|
|
138
|
+
import { requestReview } from '@symbiote-native/store-review/angular';
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Notes
|
|
142
|
+
|
|
143
|
+
- **Deliberate deviation from upstream — no `expo-constants` dependency.** Upstream's
|
|
144
|
+
`storeUrl()` reads `Constants.expoConfig.ios.appStoreUrl`/`.android.playStoreUrl` off
|
|
145
|
+
`expo-constants`, which requires an Expo-CLI-generated manifest (`app.config`/EAS/updates)
|
|
146
|
+
this bare, Metro-only project never produces (see the `symbiote-expo-package-catalog` skill,
|
|
147
|
+
row 9 — `expo-constants` was skipped for exactly this reason). So this port trims that surface:
|
|
148
|
+
every function that would have read the manifest instead takes an optional
|
|
149
|
+
`IStoreReviewUrlOptions` argument (`iosAppStoreUrl` / `androidPlayStoreUrl`), and the caller
|
|
150
|
+
supplies the store URL explicitly instead of it being resolved from `app.json`/`app.config.js`.
|
|
151
|
+
There is no standalone `storeUrl()` export — its logic lives inline as a private helper,
|
|
152
|
+
since it now takes the same options argument every public function does.
|
|
153
|
+
- **`requestReview()` prefers the native in-app review flow** (iOS `SKStoreReviewController`,
|
|
154
|
+
Android's Play Core in-app review API) and only falls back to opening the supplied store URL
|
|
155
|
+
via `Linking` when the native flow is unavailable — matching upstream's own fallback order.
|
|
156
|
+
- **A missing URL on the fallback path logs a warning, never throws** — matching upstream's own
|
|
157
|
+
`console.warn`-and-continue behavior, just with different wording (pointing at the
|
|
158
|
+
`IStoreReviewUrlOptions` argument instead of `app.json`).
|
|
159
|
+
|
|
160
|
+
## Test it
|
|
161
|
+
|
|
162
|
+
No Fabric/Descriptor angle at all — every export here is a pure async-function surface, never a
|
|
163
|
+
view or per-instance state. Tests inject a fake native-module object in place of the real
|
|
164
|
+
`requireNativeModule` resolution, plus a stubbed `expo-modules-core` `Platform` and a stubbed
|
|
165
|
+
`react-native` `Linking` (`src/core/store-review.test.ts`, `vitest`) — no `installFabric()`, no
|
|
166
|
+
ViewConfig. Native rendering itself is verified on-device (see the parent
|
|
167
|
+
[README](../../README.md) for the project's testing model).
|
|
168
|
+
|
|
169
|
+
Native autolinking wiring for `expo-modules-core` packages is already done in the four Expo
|
|
170
|
+
canary apps (`examples/expo-react`, `examples/expo-vue-sfc`, `examples/expo-vue-tsx`,
|
|
171
|
+
`examples/expo-angular`) via `@symbiote-native/local-auth`/`@symbiote-native/sensors` — this
|
|
172
|
+
package reuses that same wiring with zero further app-side changes, since
|
|
173
|
+
`expo-modules-autolinking` discovers any `expo-modules-core` package already present in
|
|
174
|
+
`node_modules`. A dedicated demo screen has not been wired into those canaries yet.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isAvailableAsync, requestReview, hasAction } from './store-review';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type IStoreReviewUrlOptions = {
|
|
2
|
+
iosAppStoreUrl?: string;
|
|
3
|
+
androidPlayStoreUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Determines if the platform has the capabilities to use `requestReview()`'s native flow.
|
|
7
|
+
* @return
|
|
8
|
+
* - On iOS, resolves to `true` unless the app is distributed through TestFlight.
|
|
9
|
+
* - On Android, resolves to `true` when the Play Store app is installed — upstream's own JSDoc
|
|
10
|
+
* claims "Android 5.0+", but its `StoreReviewModule.kt` checks only for that package.
|
|
11
|
+
*
|
|
12
|
+
* A `true` from either platform still does not mean a prompt will appear.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isAvailableAsync(): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* In ideal circumstances this opens a native modal letting the user pick a star rating that's
|
|
17
|
+
* then applied to the App Store/Play Store, without leaving the app. If the native flow is
|
|
18
|
+
* unavailable, falls back to opening the store URL supplied via `options`.
|
|
19
|
+
*
|
|
20
|
+
* Resolving does NOT mean a prompt appeared — neither store reports that back, by design, so
|
|
21
|
+
* there is nothing to branch on. On Android it only shows for a build installed from Google
|
|
22
|
+
* Play; a sideloaded build runs the whole Play Core flow and displays nothing. Both stores also
|
|
23
|
+
* enforce a quota.
|
|
24
|
+
*/
|
|
25
|
+
export declare function requestReview(options?: IStoreReviewUrlOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* @return `true` if `requestReview()` is capable of directing the user to some kind of store
|
|
28
|
+
* review flow — either the native flow is available, or a store URL was supplied via `options`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hasAction(options?: IStoreReviewUrlOptions): Promise<boolean>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-store-review/src/StoreReview.ts (sdk-57), with
|
|
2
|
+
// one deliberate deviation: upstream's `storeUrl()` reads
|
|
3
|
+
// `Constants.expoConfig.ios.appStoreUrl`/`.android.playStoreUrl` off `expo-constants`, which this
|
|
4
|
+
// project doesn't depend on (symbiote-expo-package-catalog skill, row 9 — expo-constants hard-
|
|
5
|
+
// depends on an Expo-CLI-generated manifest a bare/Metro-only project like this one never
|
|
6
|
+
// produces). So the store URL is supplied explicitly by the caller instead of being read from a
|
|
7
|
+
// manifest — every function below takes an optional `IStoreReviewUrlOptions` in place of
|
|
8
|
+
// upstream's implicit `Constants` read.
|
|
9
|
+
import { Platform } from 'expo-modules-core';
|
|
10
|
+
import { Linking } from 'react-native';
|
|
11
|
+
import { expoStoreReview } from './native-module';
|
|
12
|
+
/**
|
|
13
|
+
* Determines if the platform has the capabilities to use `requestReview()`'s native flow.
|
|
14
|
+
* @return
|
|
15
|
+
* - On iOS, resolves to `true` unless the app is distributed through TestFlight.
|
|
16
|
+
* - On Android, resolves to `true` when the Play Store app is installed — upstream's own JSDoc
|
|
17
|
+
* claims "Android 5.0+", but its `StoreReviewModule.kt` checks only for that package.
|
|
18
|
+
*
|
|
19
|
+
* A `true` from either platform still does not mean a prompt will appear.
|
|
20
|
+
*/
|
|
21
|
+
export async function isAvailableAsync() {
|
|
22
|
+
return expoStoreReview.isAvailableAsync?.() ?? false;
|
|
23
|
+
}
|
|
24
|
+
function urlFor(options) {
|
|
25
|
+
if (Platform.OS === 'ios') {
|
|
26
|
+
return options?.iosAppStoreUrl ?? null;
|
|
27
|
+
}
|
|
28
|
+
if (Platform.OS === 'android') {
|
|
29
|
+
return options?.androidPlayStoreUrl ?? null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* In ideal circumstances this opens a native modal letting the user pick a star rating that's
|
|
35
|
+
* then applied to the App Store/Play Store, without leaving the app. If the native flow is
|
|
36
|
+
* unavailable, falls back to opening the store URL supplied via `options`.
|
|
37
|
+
*
|
|
38
|
+
* Resolving does NOT mean a prompt appeared — neither store reports that back, by design, so
|
|
39
|
+
* there is nothing to branch on. On Android it only shows for a build installed from Google
|
|
40
|
+
* Play; a sideloaded build runs the whole Play Core flow and displays nothing. Both stores also
|
|
41
|
+
* enforce a quota.
|
|
42
|
+
*/
|
|
43
|
+
export async function requestReview(options) {
|
|
44
|
+
if (expoStoreReview.requestReview) {
|
|
45
|
+
return expoStoreReview.requestReview();
|
|
46
|
+
}
|
|
47
|
+
const url = urlFor(options);
|
|
48
|
+
if (url) {
|
|
49
|
+
const supported = await Linking.canOpenURL(url);
|
|
50
|
+
if (!supported) {
|
|
51
|
+
console.warn(`StoreReview.requestReview(): Can't open store url: ${url}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await Linking.openURL(url);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.warn('StoreReview.requestReview(): no native review flow and no store URL was provided — ' +
|
|
59
|
+
'pass { iosAppStoreUrl, androidPlayStoreUrl } to requestReview().');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @return `true` if `requestReview()` is capable of directing the user to some kind of store
|
|
64
|
+
* review flow — either the native flow is available, or a store URL was supplied via `options`.
|
|
65
|
+
*/
|
|
66
|
+
export async function hasAction(options) {
|
|
67
|
+
return !!urlFor(options) || (await isAvailableAsync());
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @symbiote-native/store-review/react: the React entry over the framework-agnostic core.
|
|
2
|
+
// Upstream ships free async functions, no per-instance state and no event stream — there is
|
|
3
|
+
// nothing for a hook to wrap, so this is a plain re-export, mirroring
|
|
4
|
+
// @symbiote-native/device's/@symbiote-native/local-auth's React entry.
|
|
5
|
+
export * from '../core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../core';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @symbiote-native/store-review/angular: the Angular entry over the framework-agnostic core.
|
|
2
|
+
// Same reasoning as the React/Vue entries — no per-instance state or event stream to wrap in a
|
|
3
|
+
// service, so this is a plain re-export.
|
|
4
|
+
export * from '../core';
|
|
5
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,+FAA+F;AAC/F,yCAAyC;AACzC,cAAc,SAAS,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAExD,MAAM,6BAA6B,GAAG,iBAAiB,CAAC;AAWxD,MAAM,CAAC,MAAM,eAAe,GAAG,mBAAmB,CAChD,6BAA6B,CAC9B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type IStoreReviewUrlOptions = {
|
|
2
|
+
iosAppStoreUrl?: string;
|
|
3
|
+
androidPlayStoreUrl?: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Determines if the platform has the capabilities to use `requestReview()`'s native flow.
|
|
7
|
+
* @return
|
|
8
|
+
* - On iOS, resolves to `true` unless the app is distributed through TestFlight.
|
|
9
|
+
* - On Android, resolves to `true` when the Play Store app is installed — upstream's own JSDoc
|
|
10
|
+
* claims "Android 5.0+", but its `StoreReviewModule.kt` checks only for that package.
|
|
11
|
+
*
|
|
12
|
+
* A `true` from either platform still does not mean a prompt will appear.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isAvailableAsync(): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* In ideal circumstances this opens a native modal letting the user pick a star rating that's
|
|
17
|
+
* then applied to the App Store/Play Store, without leaving the app. If the native flow is
|
|
18
|
+
* unavailable, falls back to opening the store URL supplied via `options`.
|
|
19
|
+
*
|
|
20
|
+
* Resolving does NOT mean a prompt appeared — neither store reports that back, by design, so
|
|
21
|
+
* there is nothing to branch on. On Android it only shows for a build installed from Google
|
|
22
|
+
* Play; a sideloaded build runs the whole Play Core flow and displays nothing. Both stores also
|
|
23
|
+
* enforce a quota.
|
|
24
|
+
*/
|
|
25
|
+
export declare function requestReview(options?: IStoreReviewUrlOptions): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* @return `true` if `requestReview()` is capable of directing the user to some kind of store
|
|
28
|
+
* review flow — either the native flow is available, or a store URL was supplied via `options`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function hasAction(options?: IStoreReviewUrlOptions): Promise<boolean>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-store-review/src/StoreReview.ts (sdk-57), with
|
|
2
|
+
// one deliberate deviation: upstream's `storeUrl()` reads
|
|
3
|
+
// `Constants.expoConfig.ios.appStoreUrl`/`.android.playStoreUrl` off `expo-constants`, which this
|
|
4
|
+
// project doesn't depend on (symbiote-expo-package-catalog skill, row 9 — expo-constants hard-
|
|
5
|
+
// depends on an Expo-CLI-generated manifest a bare/Metro-only project like this one never
|
|
6
|
+
// produces). So the store URL is supplied explicitly by the caller instead of being read from a
|
|
7
|
+
// manifest — every function below takes an optional `IStoreReviewUrlOptions` in place of
|
|
8
|
+
// upstream's implicit `Constants` read.
|
|
9
|
+
import { Platform } from 'expo-modules-core';
|
|
10
|
+
import { Linking } from 'react-native';
|
|
11
|
+
import { expoStoreReview } from './native-module';
|
|
12
|
+
/**
|
|
13
|
+
* Determines if the platform has the capabilities to use `requestReview()`'s native flow.
|
|
14
|
+
* @return
|
|
15
|
+
* - On iOS, resolves to `true` unless the app is distributed through TestFlight.
|
|
16
|
+
* - On Android, resolves to `true` when the Play Store app is installed — upstream's own JSDoc
|
|
17
|
+
* claims "Android 5.0+", but its `StoreReviewModule.kt` checks only for that package.
|
|
18
|
+
*
|
|
19
|
+
* A `true` from either platform still does not mean a prompt will appear.
|
|
20
|
+
*/
|
|
21
|
+
export async function isAvailableAsync() {
|
|
22
|
+
return expoStoreReview.isAvailableAsync?.() ?? false;
|
|
23
|
+
}
|
|
24
|
+
function urlFor(options) {
|
|
25
|
+
if (Platform.OS === 'ios') {
|
|
26
|
+
return options?.iosAppStoreUrl ?? null;
|
|
27
|
+
}
|
|
28
|
+
if (Platform.OS === 'android') {
|
|
29
|
+
return options?.androidPlayStoreUrl ?? null;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* In ideal circumstances this opens a native modal letting the user pick a star rating that's
|
|
35
|
+
* then applied to the App Store/Play Store, without leaving the app. If the native flow is
|
|
36
|
+
* unavailable, falls back to opening the store URL supplied via `options`.
|
|
37
|
+
*
|
|
38
|
+
* Resolving does NOT mean a prompt appeared — neither store reports that back, by design, so
|
|
39
|
+
* there is nothing to branch on. On Android it only shows for a build installed from Google
|
|
40
|
+
* Play; a sideloaded build runs the whole Play Core flow and displays nothing. Both stores also
|
|
41
|
+
* enforce a quota.
|
|
42
|
+
*/
|
|
43
|
+
export async function requestReview(options) {
|
|
44
|
+
if (expoStoreReview.requestReview) {
|
|
45
|
+
return expoStoreReview.requestReview();
|
|
46
|
+
}
|
|
47
|
+
const url = urlFor(options);
|
|
48
|
+
if (url) {
|
|
49
|
+
const supported = await Linking.canOpenURL(url);
|
|
50
|
+
if (!supported) {
|
|
51
|
+
console.warn(`StoreReview.requestReview(): Can't open store url: ${url}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
await Linking.openURL(url);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.warn('StoreReview.requestReview(): no native review flow and no store URL was provided — ' +
|
|
59
|
+
'pass { iosAppStoreUrl, androidPlayStoreUrl } to requestReview().');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* @return `true` if `requestReview()` is capable of directing the user to some kind of store
|
|
64
|
+
* review flow — either the native flow is available, or a store URL was supplied via `options`.
|
|
65
|
+
*/
|
|
66
|
+
export async function hasAction(options) {
|
|
67
|
+
return !!urlFor(options) || (await isAvailableAsync());
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=store-review.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-review.js","sourceRoot":"","sources":["../../src/core/store-review.ts"],"names":[],"mappings":"AAAA,8FAA8F;AAC9F,0DAA0D;AAC1D,kGAAkG;AAClG,+FAA+F;AAC/F,0FAA0F;AAC1F,gGAAgG;AAChG,yFAAyF;AACzF,wCAAwC;AACxC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAOlD;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,eAAe,CAAC,gBAAgB,EAAE,EAAE,IAAI,KAAK,CAAC;AACvD,CAAC;AAED,SAAS,MAAM,CAAC,OAAgC;IAC9C,IAAI,QAAQ,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;QAC1B,OAAO,OAAO,EAAE,cAAc,IAAI,IAAI,CAAC;IACzC,CAAC;IACD,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO,OAAO,EAAE,mBAAmB,IAAI,IAAI,CAAC;IAC9C,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgC;IAClE,IAAI,eAAe,CAAC,aAAa,EAAE,CAAC;QAClC,OAAO,eAAe,CAAC,aAAa,EAAE,CAAC;IACzC,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IAC5B,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAChD,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,sDAAsD,GAAG,EAAE,CAAC,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,IAAI,CACV,qFAAqF;YACnF,kEAAkE,CACrE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAgC;IAC9D,OAAO,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,gBAAgB,EAAE,CAAC,CAAC;AACzD,CAAC"}
|
package/native-link.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@symbiote-native/store-review",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "expo-store-review wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Prompts the platform's native in-app review flow, with a caller-supplied store-URL fallback in place of upstream's expo-constants-based lookup (this project doesn't depend on expo-constants).",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/OneEyed1366/symbiote-native.git",
|
|
9
|
+
"directory": "packages/store-review"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/OneEyed1366/symbiote-native/tree/master/packages/store-review#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/OneEyed1366/symbiote-native/issues"
|
|
14
|
+
},
|
|
15
|
+
"author": "Andrey Prokopenko <psevdoproger@gmail.com>",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./build/core/index.js",
|
|
18
|
+
"module": "./build/core/index.js",
|
|
19
|
+
"types": "./build/core/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./build/core/index.d.ts",
|
|
23
|
+
"default": "./build/core/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./vue": {
|
|
26
|
+
"types": "./build/vue/index.d.ts",
|
|
27
|
+
"default": "./build/vue/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./build/react/index.d.ts",
|
|
31
|
+
"default": "./build/react/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./angular": {
|
|
34
|
+
"types": "./build-ngc/angular/index.d.ts",
|
|
35
|
+
"react-native": "./build-ngc/angular/index.js",
|
|
36
|
+
"default": "./src/angular/index.ts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"build",
|
|
42
|
+
"build-ngc",
|
|
43
|
+
"native-link.json"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"expo-store-review": "57.0.1",
|
|
50
|
+
"expo-modules-core": "57.0.5"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@symbiote-native/engine": ">=0.1.7",
|
|
54
|
+
"@angular/core": ">=20",
|
|
55
|
+
"@vue/runtime-core": "^3.5.13",
|
|
56
|
+
"react": ">=19.0.0",
|
|
57
|
+
"react-native": ">=0.86",
|
|
58
|
+
"vue": ">=3.5.0",
|
|
59
|
+
"@symbiote-native/react": "0.2.8",
|
|
60
|
+
"@symbiote-native/angular": "0.6.1",
|
|
61
|
+
"@symbiote-native/vue": "0.3.8"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@symbiote-native/angular": {
|
|
65
|
+
"optional": true
|
|
66
|
+
},
|
|
67
|
+
"@symbiote-native/react": {
|
|
68
|
+
"optional": true
|
|
69
|
+
},
|
|
70
|
+
"@symbiote-native/vue": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@angular/core": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@vue/runtime-core": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"react": {
|
|
80
|
+
"optional": true
|
|
81
|
+
},
|
|
82
|
+
"vue": {
|
|
83
|
+
"optional": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@angular/compiler": "^22",
|
|
88
|
+
"@angular/compiler-cli": "^22",
|
|
89
|
+
"@angular/core": "^22",
|
|
90
|
+
"@types/node": "^26.0.0",
|
|
91
|
+
"@types/react": "^19.2.0",
|
|
92
|
+
"@vue/runtime-core": "^3.5.13",
|
|
93
|
+
"react": "19.2.3",
|
|
94
|
+
"typescript": "~6.0.0",
|
|
95
|
+
"@symbiote-native/angular": "0.6.1",
|
|
96
|
+
"@symbiote-native/react": "0.2.8",
|
|
97
|
+
"@symbiote-native/vue": "0.3.8",
|
|
98
|
+
"@symbiote-native/test-utils": "0.1.6",
|
|
99
|
+
"@symbiote-native/engine": "0.1.7"
|
|
100
|
+
},
|
|
101
|
+
"scripts": {
|
|
102
|
+
"typecheck": "tsc --build",
|
|
103
|
+
"clean": "rm -rf build-ngc",
|
|
104
|
+
"ng:build": "pnpm run clean && ngc -p tsconfig.angular.json",
|
|
105
|
+
"format": "prettier --write \"src/**/*.{ts,tsx}\""
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo-modules-core';
|
|
2
|
+
|
|
3
|
+
const EXPO_STORE_REVIEW_MODULE_NAME = 'ExpoStoreReview';
|
|
4
|
+
|
|
5
|
+
// Both methods are optional for the same reason every sibling package's native module type is
|
|
6
|
+
// (device, local-auth, cellular): the native review flow itself is platform-conditional
|
|
7
|
+
// (iOS TestFlight builds, Android below 5.0), so store-review.ts checks for presence itself and
|
|
8
|
+
// falls back to a store-URL open rather than assuming the whole surface is implemented.
|
|
9
|
+
export type INativeStoreReviewModule = {
|
|
10
|
+
isAvailableAsync?(): Promise<boolean>;
|
|
11
|
+
requestReview?(): Promise<void>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const expoStoreReview = requireNativeModule<INativeStoreReviewModule>(
|
|
15
|
+
EXPO_STORE_REVIEW_MODULE_NAME,
|
|
16
|
+
);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
const FAKE_NATIVE_STORE_REVIEW = {
|
|
4
|
+
isAvailableAsync: vi.fn(async () => true),
|
|
5
|
+
requestReview: vi.fn(async () => undefined),
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const fakePlatform = { OS: 'ios' as 'ios' | 'android' | 'web' };
|
|
9
|
+
|
|
10
|
+
const fakeLinking = {
|
|
11
|
+
canOpenURL: vi.fn(async () => true),
|
|
12
|
+
openURL: vi.fn(async () => undefined),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// The real ExpoStoreReview native module only exists on device — resolving it via
|
|
16
|
+
// requireNativeModule() at import time would throw in this headless test run, so the
|
|
17
|
+
// module-lookup file is faked in place of expo-modules-core's runtime resolution, the same
|
|
18
|
+
// pattern packages/device/src/core/device.test.ts uses.
|
|
19
|
+
vi.mock('./native-module', () => ({
|
|
20
|
+
expoStoreReview: FAKE_NATIVE_STORE_REVIEW,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// expo-modules-core's real entry transitively imports 'react-native' for Platform/
|
|
24
|
+
// TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
|
|
25
|
+
// packages/application/src/core/application.test.ts uses.
|
|
26
|
+
vi.mock('expo-modules-core', () => ({
|
|
27
|
+
Platform: fakePlatform,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
// react-native's own Linking is only reached by the URL-fallback branch (no native review flow
|
|
31
|
+
// available) — stubbed the same way packages/system-ui/src/core/system-ui.test.ts stubs
|
|
32
|
+
// react-native's Platform/processColor.
|
|
33
|
+
vi.mock('react-native', () => ({
|
|
34
|
+
Linking: fakeLinking,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const { isAvailableAsync, requestReview, hasAction } = await import('./store-review');
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
fakePlatform.OS = 'ios';
|
|
41
|
+
FAKE_NATIVE_STORE_REVIEW.isAvailableAsync = vi.fn(async () => true);
|
|
42
|
+
FAKE_NATIVE_STORE_REVIEW.requestReview = vi.fn(async () => undefined);
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('isAvailableAsync', () => {
|
|
47
|
+
it('delegates to the native module', async () => {
|
|
48
|
+
await expect(isAvailableAsync()).resolves.toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('resolves false instead of throwing when the native method is absent', async () => {
|
|
52
|
+
// @ts-expect-error -- simulating a platform where the native module has no such method
|
|
53
|
+
FAKE_NATIVE_STORE_REVIEW.isAvailableAsync = undefined;
|
|
54
|
+
|
|
55
|
+
await expect(isAvailableAsync()).resolves.toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('requestReview — native review path', () => {
|
|
60
|
+
it('calls the native requestReview and never touches Linking', async () => {
|
|
61
|
+
await requestReview({ iosAppStoreUrl: 'https://apps.apple.com/app/id123' });
|
|
62
|
+
|
|
63
|
+
expect(FAKE_NATIVE_STORE_REVIEW.requestReview).toHaveBeenCalled();
|
|
64
|
+
expect(fakeLinking.canOpenURL).not.toHaveBeenCalled();
|
|
65
|
+
expect(fakeLinking.openURL).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('requestReview — URL-fallback path (no native review flow)', () => {
|
|
70
|
+
it('opens the supplied iOS App Store URL when Linking can open it', async () => {
|
|
71
|
+
// @ts-expect-error -- simulating a platform with no native review flow
|
|
72
|
+
FAKE_NATIVE_STORE_REVIEW.requestReview = undefined;
|
|
73
|
+
|
|
74
|
+
await requestReview({ iosAppStoreUrl: 'https://apps.apple.com/app/id123' });
|
|
75
|
+
|
|
76
|
+
expect(fakeLinking.canOpenURL).toHaveBeenCalledWith('https://apps.apple.com/app/id123');
|
|
77
|
+
expect(fakeLinking.openURL).toHaveBeenCalledWith('https://apps.apple.com/app/id123');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('opens the supplied Android Play Store URL on android', async () => {
|
|
81
|
+
// @ts-expect-error -- simulating a platform with no native review flow
|
|
82
|
+
FAKE_NATIVE_STORE_REVIEW.requestReview = undefined;
|
|
83
|
+
fakePlatform.OS = 'android';
|
|
84
|
+
|
|
85
|
+
await requestReview({
|
|
86
|
+
androidPlayStoreUrl: 'https://play.google.com/store/apps/details?id=com.app',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(fakeLinking.canOpenURL).toHaveBeenCalledWith(
|
|
90
|
+
'https://play.google.com/store/apps/details?id=com.app',
|
|
91
|
+
);
|
|
92
|
+
expect(fakeLinking.openURL).toHaveBeenCalledWith(
|
|
93
|
+
'https://play.google.com/store/apps/details?id=com.app',
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('warns and skips openURL when Linking reports the URL cannot be opened', async () => {
|
|
98
|
+
// @ts-expect-error -- simulating a platform with no native review flow
|
|
99
|
+
FAKE_NATIVE_STORE_REVIEW.requestReview = undefined;
|
|
100
|
+
fakeLinking.canOpenURL.mockResolvedValueOnce(false);
|
|
101
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
102
|
+
|
|
103
|
+
await requestReview({ iosAppStoreUrl: 'https://apps.apple.com/app/id123' });
|
|
104
|
+
|
|
105
|
+
expect(fakeLinking.openURL).not.toHaveBeenCalled();
|
|
106
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
107
|
+
"StoreReview.requestReview(): Can't open store url: https://apps.apple.com/app/id123",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
warnSpy.mockRestore();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('warns and never touches Linking when no URL was provided', async () => {
|
|
114
|
+
// @ts-expect-error -- simulating a platform with no native review flow
|
|
115
|
+
FAKE_NATIVE_STORE_REVIEW.requestReview = undefined;
|
|
116
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
|
117
|
+
|
|
118
|
+
await requestReview();
|
|
119
|
+
|
|
120
|
+
expect(fakeLinking.canOpenURL).not.toHaveBeenCalled();
|
|
121
|
+
expect(fakeLinking.openURL).not.toHaveBeenCalled();
|
|
122
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining('no native review flow and no store URL was provided'),
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
warnSpy.mockRestore();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('hasAction', () => {
|
|
131
|
+
it('resolves true when a store URL was supplied, without checking native availability', async () => {
|
|
132
|
+
// @ts-expect-error -- simulating a platform with no native review flow
|
|
133
|
+
FAKE_NATIVE_STORE_REVIEW.isAvailableAsync = undefined;
|
|
134
|
+
|
|
135
|
+
await expect(hasAction({ iosAppStoreUrl: 'https://apps.apple.com/app/id123' })).resolves.toBe(
|
|
136
|
+
true,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('falls back to native availability when no store URL was supplied', async () => {
|
|
141
|
+
await expect(hasAction()).resolves.toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('resolves false when there is neither a store URL nor a native review flow', async () => {
|
|
145
|
+
// @ts-expect-error -- simulating a platform with no native review flow
|
|
146
|
+
FAKE_NATIVE_STORE_REVIEW.isAvailableAsync = undefined;
|
|
147
|
+
|
|
148
|
+
await expect(hasAction()).resolves.toBe(false);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// Hand-ported from .vendors/expo/packages/expo-store-review/src/StoreReview.ts (sdk-57), with
|
|
2
|
+
// one deliberate deviation: upstream's `storeUrl()` reads
|
|
3
|
+
// `Constants.expoConfig.ios.appStoreUrl`/`.android.playStoreUrl` off `expo-constants`, which this
|
|
4
|
+
// project doesn't depend on (symbiote-expo-package-catalog skill, row 9 — expo-constants hard-
|
|
5
|
+
// depends on an Expo-CLI-generated manifest a bare/Metro-only project like this one never
|
|
6
|
+
// produces). So the store URL is supplied explicitly by the caller instead of being read from a
|
|
7
|
+
// manifest — every function below takes an optional `IStoreReviewUrlOptions` in place of
|
|
8
|
+
// upstream's implicit `Constants` read.
|
|
9
|
+
import { Platform } from 'expo-modules-core';
|
|
10
|
+
import { Linking } from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { expoStoreReview } from './native-module';
|
|
13
|
+
|
|
14
|
+
export type IStoreReviewUrlOptions = {
|
|
15
|
+
iosAppStoreUrl?: string;
|
|
16
|
+
androidPlayStoreUrl?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determines if the platform has the capabilities to use `requestReview()`'s native flow.
|
|
21
|
+
* @return
|
|
22
|
+
* - On iOS, resolves to `true` unless the app is distributed through TestFlight.
|
|
23
|
+
* - On Android, resolves to `true` when the Play Store app is installed — upstream's own JSDoc
|
|
24
|
+
* claims "Android 5.0+", but its `StoreReviewModule.kt` checks only for that package.
|
|
25
|
+
*
|
|
26
|
+
* A `true` from either platform still does not mean a prompt will appear.
|
|
27
|
+
*/
|
|
28
|
+
export async function isAvailableAsync(): Promise<boolean> {
|
|
29
|
+
return expoStoreReview.isAvailableAsync?.() ?? false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function urlFor(options?: IStoreReviewUrlOptions): string | null {
|
|
33
|
+
if (Platform.OS === 'ios') {
|
|
34
|
+
return options?.iosAppStoreUrl ?? null;
|
|
35
|
+
}
|
|
36
|
+
if (Platform.OS === 'android') {
|
|
37
|
+
return options?.androidPlayStoreUrl ?? null;
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* In ideal circumstances this opens a native modal letting the user pick a star rating that's
|
|
44
|
+
* then applied to the App Store/Play Store, without leaving the app. If the native flow is
|
|
45
|
+
* unavailable, falls back to opening the store URL supplied via `options`.
|
|
46
|
+
*
|
|
47
|
+
* Resolving does NOT mean a prompt appeared — neither store reports that back, by design, so
|
|
48
|
+
* there is nothing to branch on. On Android it only shows for a build installed from Google
|
|
49
|
+
* Play; a sideloaded build runs the whole Play Core flow and displays nothing. Both stores also
|
|
50
|
+
* enforce a quota.
|
|
51
|
+
*/
|
|
52
|
+
export async function requestReview(options?: IStoreReviewUrlOptions): Promise<void> {
|
|
53
|
+
if (expoStoreReview.requestReview) {
|
|
54
|
+
return expoStoreReview.requestReview();
|
|
55
|
+
}
|
|
56
|
+
const url = urlFor(options);
|
|
57
|
+
if (url) {
|
|
58
|
+
const supported = await Linking.canOpenURL(url);
|
|
59
|
+
if (!supported) {
|
|
60
|
+
console.warn(`StoreReview.requestReview(): Can't open store url: ${url}`);
|
|
61
|
+
} else {
|
|
62
|
+
await Linking.openURL(url);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
console.warn(
|
|
66
|
+
'StoreReview.requestReview(): no native review flow and no store URL was provided — ' +
|
|
67
|
+
'pass { iosAppStoreUrl, androidPlayStoreUrl } to requestReview().',
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @return `true` if `requestReview()` is capable of directing the user to some kind of store
|
|
74
|
+
* review flow — either the native flow is available, or a store URL was supplied via `options`.
|
|
75
|
+
*/
|
|
76
|
+
export async function hasAction(options?: IStoreReviewUrlOptions): Promise<boolean> {
|
|
77
|
+
return !!urlFor(options) || (await isAvailableAsync());
|
|
78
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @symbiote-native/store-review/react: the React entry over the framework-agnostic core.
|
|
2
|
+
// Upstream ships free async functions, no per-instance state and no event stream — there is
|
|
3
|
+
// nothing for a hook to wrap, so this is a plain re-export, mirroring
|
|
4
|
+
// @symbiote-native/device's/@symbiote-native/local-auth's React entry.
|
|
5
|
+
export * from '../core';
|
package/src/vue/index.ts
ADDED