Skip to content

Commit 2c71f76

Browse files
feat: add entitlements support to the React Native SDK
Wire the native entitlements APIs through the RN bridge and JS API. The feature already ships in the native SDKs this package bundles (frontegg-ios-swift 1.3.10 and frontegg-android-kotlin 1.3.34), so no native version bump is needed — this just exposes them to JS. - Android (FronteggRNModule.kt): loadEntitlements / getFeatureEntitlement / getPermissionEntitlement @ReactMethods calling FronteggAuth, serializing Entitlement -> { isEntitled, justification }. - iOS (FronteggRN.swift + .m): matching @objc bridge methods. - JS (FronteggNative.ts + index.tsx): loadEntitlements(forceRefresh?), getFeatureEntitlement(key), getPermissionEntitlement(key), and the Entitlement type. - docs/usage.md: Entitlements usage section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4500e94 commit 2c71f76

6 files changed

Lines changed: 141 additions & 0 deletions

File tree

android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.facebook.react.common.LifecycleState
1212
import com.facebook.react.modules.core.DeviceEventManagerModule
1313
import com.frontegg.android.AdminPortalActivity
1414
import com.frontegg.android.fronteggAuth
15+
import com.frontegg.android.models.Entitlement
1516
import io.reactivex.rxjava3.core.Observable
1617
import io.reactivex.rxjava3.disposables.Disposable
1718
import kotlin.time.DurationUnit
@@ -229,6 +230,29 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) :
229230
promise.resolve(null)
230231
}
231232

233+
@ReactMethod
234+
fun loadEntitlements(forceRefresh: Boolean, promise: Promise) {
235+
auth.loadEntitlements(forceRefresh) { success ->
236+
promise.resolve(success)
237+
}
238+
}
239+
240+
@ReactMethod
241+
fun getFeatureEntitlement(key: String, promise: Promise) {
242+
promise.resolve(entitlementToMap(auth.getFeatureEntitlements(key)))
243+
}
244+
245+
@ReactMethod
246+
fun getPermissionEntitlement(key: String, promise: Promise) {
247+
promise.resolve(entitlementToMap(auth.getPermissionEntitlements(key)))
248+
}
249+
250+
private fun entitlementToMap(entitlement: Entitlement): WritableMap =
251+
Arguments.createMap().apply {
252+
putBoolean("isEntitled", entitlement.isEntitled)
253+
putString("justification", entitlement.justification)
254+
}
255+
232256
override fun getConstants(): MutableMap<String, Any?> = fronteggConstants.toMap()
233257

234258
companion object {

docs/usage.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,35 @@ export function MyScreen() {
123123
}
124124
```
125125

126+
## Entitlements
127+
128+
Gate features and permissions on-device. Call `loadEntitlements()` once the user is authenticated, then read feature/permission entitlements from the cache:
129+
130+
```tsx
131+
import {
132+
loadEntitlements,
133+
getFeatureEntitlement,
134+
getPermissionEntitlement,
135+
} from '@frontegg/react-native';
136+
137+
// Load (or refresh) the current user's entitlements into the SDK cache.
138+
await loadEntitlements();
139+
140+
// Check a feature-flag entitlement.
141+
const feature = await getFeatureEntitlement('my-feature-key');
142+
if (feature.isEntitled) {
143+
// show the gated feature
144+
} else {
145+
console.log('Not entitled:', feature.justification);
146+
}
147+
148+
// Check a permission entitlement.
149+
const permission = await getPermissionEntitlement('fe.secure.read.users');
150+
console.log(permission.isEntitled, permission.justification);
151+
```
152+
153+
Each call resolves to an `Entitlement``{ isEntitled: boolean; justification?: string | null }`. When `isEntitled` is `false`, `justification` explains why (e.g. `"NOT_AUTHENTICATED"`, `"ENTITLEMENTS_NOT_LOADED"`, `"MISSING_FEATURE"`, `"MISSING_PERMISSION"`).
154+
126155
## Check user authentication state
127156

128157
Use the `useAuth` hook with `showLoader` and `isLoading` to conditionally display your app:

ios/FronteggRN.m

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,19 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter)
5858
openAdminPortal: (RCTPromiseResolveBlock)resolve
5959
rejecter: (RCTPromiseRejectBlock)reject
6060
)
61+
RCT_EXTERN_METHOD(
62+
loadEntitlements: (BOOL)forceRefresh
63+
resolver: (RCTPromiseResolveBlock)resolve
64+
rejecter: (RCTPromiseRejectBlock)reject
65+
)
66+
RCT_EXTERN_METHOD(
67+
getFeatureEntitlement: (NSString *)key
68+
resolver: (RCTPromiseResolveBlock)resolve
69+
rejecter: (RCTPromiseRejectBlock)reject
70+
)
71+
RCT_EXTERN_METHOD(
72+
getPermissionEntitlement: (NSString *)key
73+
resolver: (RCTPromiseResolveBlock)resolve
74+
rejecter: (RCTPromiseRejectBlock)reject
75+
)
6176
@end

ios/FronteggRN.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,42 @@ class FronteggRN: RCTEventEmitter {
294294
}
295295
}
296296

297+
@objc
298+
func loadEntitlements(
299+
_ forceRefresh: Bool,
300+
resolver: @escaping RCTPromiseResolveBlock,
301+
rejecter: @escaping RCTPromiseRejectBlock
302+
) {
303+
fronteggApp.auth.loadEntitlements(forceRefresh: forceRefresh) { success in
304+
resolver(success)
305+
}
306+
}
307+
308+
@objc
309+
func getFeatureEntitlement(
310+
_ key: String,
311+
resolver: @escaping RCTPromiseResolveBlock,
312+
rejecter: @escaping RCTPromiseRejectBlock
313+
) {
314+
resolver(Self.entitlementToDict(fronteggApp.auth.getFeatureEntitlements(featureKey: key)))
315+
}
316+
317+
@objc
318+
func getPermissionEntitlement(
319+
_ key: String,
320+
resolver: @escaping RCTPromiseResolveBlock,
321+
rejecter: @escaping RCTPromiseRejectBlock
322+
) {
323+
resolver(Self.entitlementToDict(fronteggApp.auth.getPermissionEntitlements(permissionKey: key)))
324+
}
325+
326+
private static func entitlementToDict(_ entitlement: Entitlement) -> [String: Any] {
327+
return [
328+
"isEntitled": entitlement.isEntitled,
329+
"justification": entitlement.justification ?? NSNull(),
330+
]
331+
}
332+
297333
private static func topViewController() -> UIViewController? {
298334
let keyWindow = UIApplication.shared.connectedScenes
299335
.compactMap { $0 as? UIWindowScene }

src/FronteggNative.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,39 @@ export async function openAdminPortal(): Promise<void> {
8989
return FronteggRN.openAdminPortal();
9090
}
9191

92+
export interface Entitlement {
93+
/** Whether the user is entitled to the requested feature/permission. */
94+
isEntitled: boolean;
95+
/**
96+
* Optional reason when `isEntitled` is false — e.g. `"NOT_AUTHENTICATED"`,
97+
* `"ENTITLEMENTS_NOT_LOADED"`, `"MISSING_FEATURE"`, `"MISSING_PERMISSION"`.
98+
*/
99+
justification?: string | null;
100+
}
101+
102+
/**
103+
* Loads the current user's entitlements into the SDK cache. Call this after
104+
* authentication (and again with `forceRefresh` to refetch) before reading
105+
* feature/permission entitlements. Resolves to `true` when entitlements loaded.
106+
*/
107+
export async function loadEntitlements(
108+
forceRefresh: boolean = false
109+
): Promise<boolean> {
110+
return FronteggRN.loadEntitlements(forceRefresh);
111+
}
112+
113+
/** Returns the on-device entitlement for a feature-flag key. */
114+
export async function getFeatureEntitlement(key: string): Promise<Entitlement> {
115+
return FronteggRN.getFeatureEntitlement(key);
116+
}
117+
118+
/** Returns the on-device entitlement for a permission key. */
119+
export async function getPermissionEntitlement(
120+
key: string
121+
): Promise<Entitlement> {
122+
return FronteggRN.getPermissionEntitlement(key);
123+
}
124+
92125
function debounce<T extends (...args: any[]) => any>(func: T, waitFor: number) {
93126
let timeout: any;
94127

src/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ export {
1414
isSteppedUp,
1515
stepUp,
1616
openAdminPortal,
17+
loadEntitlements,
18+
getFeatureEntitlement,
19+
getPermissionEntitlement,
1720
} from './FronteggNative';
21+
export type { Entitlement } from './FronteggNative';

0 commit comments

Comments
 (0)