Skip to content

Commit da60716

Browse files
feat: add entitlements support to the React Native SDK (#82)
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 bf98137 commit da60716

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)