Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .cursor/rules/feature-flag-guidelines.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
globs: "**/*"
alwaysApply: true
---

# Feature Flag Guidelines

## Core Principle

**ALWAYS** use the `useFeatureFlag` hook instead of creating new feature flag selectors.

## Forbidden Patterns

### ❌ NEVER Create New Feature Flag Selectors

**DO NOT** create new selectors using `createSelector` for feature flags:

```typescript
// ❌ FORBIDDEN - Do not create new feature flag selectors
export const selectMyFeatureEnabledFlag = createSelector(
selectRemoteFeatureFlags,
(remoteFeatureFlags) => {
// ... selector logic
},
);
```

**DO NOT** add new feature flag selectors in:
- `app/selectors/featureFlagController/**/*.ts`
- `app/components/**/selectors/featureFlags/**/*.ts`
- Any other location that creates feature flag selectors

## Required Pattern

### ✅ ALWAYS Use the `useFeatureFlag` Hook

**MUST** use the `useFeatureFlag` hook from `app/components/hooks/FeatureFlags/useFeatureFlag.ts`:

```typescript
// ✅ REQUIRED - Use the hook instead
import { useFeatureFlag, FeatureFlagNames } from '../../../hooks/FeatureFlags/useFeatureFlag';

const MyComponent = () => {
const isFeatureEnabled = useFeatureFlag(FeatureFlagNames.rewardsEnabled);

// Use the flag value
if (isFeatureEnabled) {
// ... feature logic
}
};
```

## Steps to Use Feature Flags

1. **Add the flag name** to the `FeatureFlagNames` enum in `app/components/hooks/FeatureFlags/useFeatureFlag.ts`:
```typescript
export enum FeatureFlagNames {
rewardsEnabled = 'rewardsEnabled',
myNewFeature = 'myNewFeature', // Add your new flag here
}
```

2. **Use the hook** in your component:
```typescript
const isMyFeatureEnabled = useFeatureFlag(FeatureFlagNames.myNewFeature);
```

3. **Do NOT** create a selector for the feature flag

## Migration Pattern

If you encounter existing feature flag selectors, prefer migrating to the hook:

```typescript
// ❌ Old pattern (existing code - do not replicate)
const isFeatureEnabled = useSelector(selectMyFeatureEnabledFlag);

// ✅ New pattern (use this instead)
const isFeatureEnabled = useFeatureFlag(FeatureFlagNames.myNewFeature);
```

## Enforcement

- **REJECT** any code that creates new `createSelector` instances for feature flags
- **REJECT** any new files in `app/selectors/featureFlagController/` directories
- **REQUIRE** use of `useFeatureFlag` hook for all feature flag access
- **REQUIRE** adding flag names to `FeatureFlagNames` enum before use

## Exception

The only exception is the base selector `selectRemoteFeatureFlags` in `app/selectors/featureFlagController/index.ts`, which is used internally by the `useFeatureFlag` hook infrastructure.
10 changes: 6 additions & 4 deletions .github/workflows/release-pr-approval.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ name: Release PR Approval

on:
pull_request:
branches:
- Version-v*
- release/*
pull_request_review:
types: [submitted]

jobs:
release-pr-approval:
if: >
startsWith(github.event.pull_request.base.ref, 'Version-v') ||
startsWith(github.event.pull_request.base.ref, 'release/')
runs-on: ubuntu-latest
steps:
- name: Require Release Team approval
uses: op5dev/require-team-approval@dfd7b8b9a88bf82a955c103f7e19642b0411aecd
with:
team: release-team
token: ${{ secrets.METAMASK_MOBILE_ORG_READ_TOKEN }}
token: ${{ secrets.METAMASK_MOBILE_ORG_READ_TOKEN }}
177 changes: 177 additions & 0 deletions .yarn/patches/rive-react-native-npm-9.3.4-8082feca90.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
diff --git a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
index 25746a8300c5c85889a263f8afeb703a08e834b0..5a1f97f3166d76aaebec5abd25997bd923ca11d0 100644
--- a/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
+++ b/android/src/main/java/com/rivereactnative/RiveReactNativeView.kt
@@ -993,13 +993,12 @@ class RiveReactNativeView(private val context: ThemedReactContext) : FrameLayout
return
}

- val queue = Volley.newRequestQueue(context)
-
- val stringRequest = RNRiveFileRequest(
- url, listener
- ) { error -> handleURLAssetError(url, error, isUserHandlingErrors) }
-
- queue.add(stringRequest)
+ val loader = ResourceLoaderFactory.getLoader(url, context)
+ loader.loadResource(
+ url,
+ listener,
+ { error -> handleURLAssetError(url, error, isUserHandlingErrors) }
+ )
}

private fun processAssetBytes(bytes: ByteArray, asset: FileAsset) {
@@ -1220,3 +1219,62 @@ data class PropertyListener(
val propertyType: String,
val job: Job
)
+
+
+interface ResourceLoader {
+ fun loadResource(
+ url: String,
+ listener: Response.Listener<ByteArray>,
+ errorListener: Response.ErrorListener
+ )
+}
+
+// Standard Volley HTTP implementation
+class VolleyHttpLoader(private val context: ThemedReactContext) : ResourceLoader {
+ override fun loadResource(
+ url: String,
+ listener: Response.Listener<ByteArray>,
+ errorListener: Response.ErrorListener
+ ) {
+ // Use your existing RNRiveFileRequest for HTTP
+ val queue = Volley.newRequestQueue(context)
+
+ val request = RNRiveFileRequest(
+ url, listener, errorListener
+ )
+
+ queue.add(request)
+ }
+}
+
+// Direct file system implementation
+class FileSystemLoader : ResourceLoader {
+ override fun loadResource(
+ url: String,
+ listener: Response.Listener<ByteArray>,
+ errorListener: Response.ErrorListener
+ ) {
+ try {
+ // Extract file path from file:// URL
+ val filePath = url.substring(7) // Remove "file://"
+ val file = java.io.File(filePath)
+
+ // Read file directly
+ val data = file.readBytes()
+ listener.onResponse(data)
+ } catch (e: Exception) {
+ // Pretend the error came from Volley, which is how http URLs are loaded
+ errorListener.onErrorResponse(VolleyError(e))
+ }
+ }
+}
+
+// Factory class that returns the appropriate loader
+object ResourceLoaderFactory {
+ fun getLoader(url: String, context: ThemedReactContext): ResourceLoader {
+ return when {
+ url.startsWith("file://") -> FileSystemLoader()
+ else -> VolleyHttpLoader(context)
+ }
+ }
+}
diff --git a/ios/RiveReactNativeView.swift b/ios/RiveReactNativeView.swift
index 4a218ed11dc662b023554fe2e4ff54eb57a9d35f..348f798359b046201252b1b841b9f2fb8fe1d3e4 100644
--- a/ios/RiveReactNativeView.swift
+++ b/ios/RiveReactNativeView.swift
@@ -346,7 +346,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
}
resourceName = nil
resourceFromBundle = false
- downloadUrlAsset(url: url) { [weak self] data in
+ loadUrlAsset(url: url) { [weak self] data in
guard let self = self else { return }
guard !data.isEmpty else {
handleRiveError(error: createIncorrectRiveURL(url))
@@ -472,13 +472,13 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
return
}

- downloadUrlAsset(url: sourceAssetId) { [weak self] data in
+ loadUrlAsset(url: sourceAssetId) { [weak self] data in
self?.processAssetBytes(data, asset: asset, factory: factory)
}
}

private func handleSourceUrl(_ sourceUrl: String, asset: RiveFileAsset, factory: RiveFactory) {
- downloadUrlAsset(url: sourceUrl) { [weak self] data in
+ loadUrlAsset(url: sourceUrl) { [weak self] data in
self?.processAssetBytes(data, asset: asset, factory: factory)
}
}
@@ -516,22 +516,46 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate
}
}

- private func downloadUrlAsset(url: String, listener: @escaping (Data) -> Void) {
+ private func loadUrlAsset(url: String, listener: @escaping (Data) -> Void) {
guard isValidUrl(url) else {
handleInvalidUrlError(url: url)
return
}

- let queue = URLSession.shared
- guard let requestUrl = URL(string: url) else {
+ guard let assetUrl = URL(string: url) else {
handleInvalidUrlError(url: url)
return
}

- let request = URLRequest(url: requestUrl)
+ if assetUrl.isFileURL {
+ loadFileUrlAsset(url: assetUrl, listener: listener)
+ } else {
+ loadRemoteUrlAsset(url: assetUrl, listener: listener)
+ }
+ }
+
+ private func loadFileUrlAsset(url: URL, listener: @escaping (Data) -> Void) {
+ DispatchQueue.global(qos: .background).async { [weak self] in
+ do {
+ let fileData = try Data(contentsOf: url)
+ DispatchQueue.main.async {
+ listener(fileData)
+ }
+ } catch {
+ DispatchQueue.main.async {
+ self?.handleInvalidUrlError(url: url.absoluteString)
+ }
+ }
+ }
+ }
+
+ private func loadRemoteUrlAsset(url: URL, listener: @escaping (Data) -> Void) {
+ let queue = URLSession.shared
+ let request = URLRequest(url: url)
+
let task = queue.dataTask(with: request) {[weak self] data, response, error in
if error != nil {
- self?.handleInvalidUrlError(url: url)
+ self?.handleInvalidUrlError(url: url.absoluteString)
} else if let data = data {
listener(data)
}
@@ -542,7 +566,7 @@ class RiveReactNativeView: RCTView, RivePlayerDelegate, RiveStateMachineDelegate

private func isValidUrl(_ url: String) -> Bool {
if let url = URL(string: url) {
- return UIApplication.shared.canOpenURL(url)
+ return url.isFileURL || UIApplication.shared.canOpenURL(url)
} else {
return false
}
15 changes: 12 additions & 3 deletions app/__mocks__/expo-updates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@ export const channel = 'test-channel';
export const runtimeVersion = '1.0.0';
export const isEmbeddedLaunch = true;
export const isEnabled = true;
export const url = 'https://example.com';
export const checkAutomatically = 'NEVER';
export const updateId = 'mock-update-id';

export const checkForUpdateAsync = jest.fn();
export const fetchUpdateAsync = jest.fn();
export const reloadAsync = jest.fn();
export const checkForUpdateAsync = jest.fn().mockResolvedValue({
isAvailable: false,
manifest: null,
});
export const fetchUpdateAsync = jest.fn().mockResolvedValue({
isNew: false,
});
export const reloadAsync = jest.fn().mockResolvedValue(undefined);
export const useUpdates = jest.fn();

export const UpdateEventType = {
Expand All @@ -26,7 +33,9 @@ export default {
runtimeVersion,
isEmbeddedLaunch,
isEnabled,
url,
updateId,
checkAutomatically,
checkForUpdateAsync,
fetchUpdateAsync,
reloadAsync,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React from 'react';
import { useStyles } from '../../../hooks';
import HeaderBase from '../../HeaderBase';

import ButtonIcon from '../../Buttons/ButtonIcon';
import ButtonIcon, { ButtonIconSizes } from '../../Buttons/ButtonIcon';
import { IconName, IconColor } from '../../Icons/Icon';

// Internal dependencies.
Expand Down Expand Up @@ -35,6 +35,7 @@ const BottomSheetHeader: React.FC<BottomSheetHeaderProps> = ({
iconName={IconName.ArrowLeft}
iconColor={IconColor.Default}
onPress={onBack}
size={ButtonIconSizes.Lg}
{...backButtonProps}
/>
);
Expand All @@ -44,6 +45,7 @@ const BottomSheetHeader: React.FC<BottomSheetHeaderProps> = ({
iconName={IconName.Close}
iconColor={IconColor.Default}
onPress={onClose}
size={ButtonIconSizes.Lg}
{...closeButtonProps}
/>
);
Expand Down
Loading
Loading