diff --git a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt index 1c6b4e2..39b33b5 100644 --- a/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt +++ b/android/src/main/java/com/frontegg/reactnative/FronteggRNModule.kt @@ -12,6 +12,7 @@ import com.facebook.react.common.LifecycleState import com.facebook.react.modules.core.DeviceEventManagerModule import com.frontegg.android.AdminPortalActivity import com.frontegg.android.fronteggAuth +import com.frontegg.android.models.Entitlement import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable import kotlin.time.DurationUnit @@ -229,6 +230,29 @@ class FronteggRNModule(val reactContext: ReactApplicationContext) : promise.resolve(null) } + @ReactMethod + fun loadEntitlements(forceRefresh: Boolean, promise: Promise) { + auth.loadEntitlements(forceRefresh) { success -> + promise.resolve(success) + } + } + + @ReactMethod + fun getFeatureEntitlement(key: String, promise: Promise) { + promise.resolve(entitlementToMap(auth.getFeatureEntitlements(key))) + } + + @ReactMethod + fun getPermissionEntitlement(key: String, promise: Promise) { + promise.resolve(entitlementToMap(auth.getPermissionEntitlements(key))) + } + + private fun entitlementToMap(entitlement: Entitlement): WritableMap = + Arguments.createMap().apply { + putBoolean("isEntitled", entitlement.isEntitled) + putString("justification", entitlement.justification) + } + override fun getConstants(): MutableMap = fronteggConstants.toMap() companion object { diff --git a/docs/usage.md b/docs/usage.md index bdaa977..f1c4392 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -123,6 +123,35 @@ export function MyScreen() { } ``` +## Entitlements + +Gate features and permissions on-device. Call `loadEntitlements()` once the user is authenticated, then read feature/permission entitlements from the cache: + +```tsx +import { + loadEntitlements, + getFeatureEntitlement, + getPermissionEntitlement, +} from '@frontegg/react-native'; + +// Load (or refresh) the current user's entitlements into the SDK cache. +await loadEntitlements(); + +// Check a feature-flag entitlement. +const feature = await getFeatureEntitlement('my-feature-key'); +if (feature.isEntitled) { + // show the gated feature +} else { + console.log('Not entitled:', feature.justification); +} + +// Check a permission entitlement. +const permission = await getPermissionEntitlement('fe.secure.read.users'); +console.log(permission.isEntitled, permission.justification); +``` + +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"`). + ## Check user authentication state Use the `useAuth` hook with `showLoader` and `isLoading` to conditionally display your app: diff --git a/ios/FronteggRN.m b/ios/FronteggRN.m index 1509d78..7b91721 100644 --- a/ios/FronteggRN.m +++ b/ios/FronteggRN.m @@ -58,4 +58,19 @@ @interface RCT_EXTERN_MODULE(FronteggRN, RCTEventEmitter) openAdminPortal: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + loadEntitlements: (BOOL)forceRefresh + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) +RCT_EXTERN_METHOD( + getFeatureEntitlement: (NSString *)key + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) +RCT_EXTERN_METHOD( + getPermissionEntitlement: (NSString *)key + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject + ) @end diff --git a/ios/FronteggRN.swift b/ios/FronteggRN.swift index 7bc15cf..a3c530f 100644 --- a/ios/FronteggRN.swift +++ b/ios/FronteggRN.swift @@ -294,6 +294,42 @@ class FronteggRN: RCTEventEmitter { } } + @objc + func loadEntitlements( + _ forceRefresh: Bool, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + fronteggApp.auth.loadEntitlements(forceRefresh: forceRefresh) { success in + resolver(success) + } + } + + @objc + func getFeatureEntitlement( + _ key: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + resolver(Self.entitlementToDict(fronteggApp.auth.getFeatureEntitlements(featureKey: key))) + } + + @objc + func getPermissionEntitlement( + _ key: String, + resolver: @escaping RCTPromiseResolveBlock, + rejecter: @escaping RCTPromiseRejectBlock + ) { + resolver(Self.entitlementToDict(fronteggApp.auth.getPermissionEntitlements(permissionKey: key))) + } + + private static func entitlementToDict(_ entitlement: Entitlement) -> [String: Any] { + return [ + "isEntitled": entitlement.isEntitled, + "justification": entitlement.justification ?? NSNull(), + ] + } + private static func topViewController() -> UIViewController? { let keyWindow = UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } diff --git a/src/FronteggNative.ts b/src/FronteggNative.ts index 9c1b2db..1970eba 100644 --- a/src/FronteggNative.ts +++ b/src/FronteggNative.ts @@ -89,6 +89,39 @@ export async function openAdminPortal(): Promise { return FronteggRN.openAdminPortal(); } +export interface Entitlement { + /** Whether the user is entitled to the requested feature/permission. */ + isEntitled: boolean; + /** + * Optional reason when `isEntitled` is false — e.g. `"NOT_AUTHENTICATED"`, + * `"ENTITLEMENTS_NOT_LOADED"`, `"MISSING_FEATURE"`, `"MISSING_PERMISSION"`. + */ + justification?: string | null; +} + +/** + * Loads the current user's entitlements into the SDK cache. Call this after + * authentication (and again with `forceRefresh` to refetch) before reading + * feature/permission entitlements. Resolves to `true` when entitlements loaded. + */ +export async function loadEntitlements( + forceRefresh: boolean = false +): Promise { + return FronteggRN.loadEntitlements(forceRefresh); +} + +/** Returns the on-device entitlement for a feature-flag key. */ +export async function getFeatureEntitlement(key: string): Promise { + return FronteggRN.getFeatureEntitlement(key); +} + +/** Returns the on-device entitlement for a permission key. */ +export async function getPermissionEntitlement( + key: string +): Promise { + return FronteggRN.getPermissionEntitlement(key); +} + function debounce any>(func: T, waitFor: number) { let timeout: any; diff --git a/src/index.tsx b/src/index.tsx index d82856d..c69bd1d 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -14,4 +14,8 @@ export { isSteppedUp, stepUp, openAdminPortal, + loadEntitlements, + getFeatureEntitlement, + getPermissionEntitlement, } from './FronteggNative'; +export type { Entitlement } from './FronteggNative';