Skip to content

Refactor/general improvements#591

Merged
mCodex merged 4 commits into
masterfrom
refactor/generalImprovements
Apr 28, 2026
Merged

Refactor/general improvements#591
mCodex merged 4 commits into
masterfrom
refactor/generalImprovements

Conversation

@mCodex

@mCodex mCodex commented Apr 28, 2026

Copy link
Copy Markdown
Owner

This pull request introduces several improvements and new features for Expo and documentation, as well as project-wide configuration enhancements. The main changes include a new Expo config plugin for handling platform-specific permissions and new architecture flags, comprehensive documentation for both the Expo integration and the overall package architecture, and clarifications in the React hook usage documentation. Additionally, a project-wide .editorconfig file has been added for consistent code formatting.

Expo config plugin and tests:

  • Added a new Expo config plugin (app.plugin.js) that:
    • Configures New Architecture flags for both iOS and Android.
    • Sets the NSFaceIDUsageDescription in iOS Info.plist, with support for custom or skipped values.
    • Adds both USE_BIOMETRIC and legacy USE_FINGERPRINT permissions to AndroidManifest.xml, ensuring idempotency and correct version targeting.
    • Allows plugin options for customizing Face ID permission text and toggling New Architecture flags. [1] [2]
  • Added comprehensive unit tests for the Expo config plugin, covering all supported options, idempotency, and correct permission handling.

Documentation improvements:

  • Added docs/EXPO.md with detailed installation, configuration, and troubleshooting instructions for Expo users, including a compatibility matrix and plugin options.
  • Added docs/ARCHITECTURE.md documenting the overall package structure, naming conventions, cache strategy, and tree-shaking approach.
  • Updated docs/HOOKS.md to clarify that hook caching is per-instance (not global), improved code examples, and explained best practices for sharing capability data between components. [1] [2] [3] [4]

Project configuration:

  • Added a .editorconfig file to enforce consistent code style (indentation, charset, line endings, etc.) across the project, with overrides for markdown and data files.

mCodex added 3 commits April 28, 2026 11:24
Rework hooks internals to improve stability and performance: replace several hook-local useState usages with reducers (useAsync, useMutation) and memoize returned objects to preserve referential identity. Add internal deepEqual and use it to stabilize option objects (useStableOptions, useSecureStorage) and avoid unnecessary callback recreations; introduce a frozen EMPTY_ITEMS fallback for stable empty lists. Update useSecurityAvailability and other hooks to return memoized state and expose createInitialAsyncState/createInitialVoidState from index. Example and docs updated: remove example/useAsyncAction, switch StorageCard to useSecureOperation with explicit handlers, and clarify per-component-instance caching and import path changes in HOOKS.md.
Introduce input validation and related infrastructure: add src/internal/validate.ts with validateKey/validateService/validateValue and byte-size limits, and wire these checks into core storage functions to throw typed InvalidArgumentError before native calls. Add InvalidArgumentError and isInvalidArgumentError to src/errors.ts and map the error code in the adapter. Improve native module loading in src/internal/native.ts with a lazy constructor and clearer error message when the native module is unavailable. Add an Expo config plugin (app.plugin.js) and typings (app.plugin.d.ts) plus unit tests (__tests__/app.plugin.test.ts) that verify Info.plist, manifest, gradle/podfile modifications. Add tests for validation (src/__tests__/internal.validate.test.ts) and small test/type adjustments (hooks/useSecret tests, fixtures). Update jest.config.js to include __tests__ at repo root and enable exactOptionalPropertyTypes in tsconfig.json. Also make several small typing/behavior fixes (explicit | undefined optional types, throw toSensitiveInfoError replacements, and dedupe manifest permission handling).
Add several new documentation pages (docs/ARCHITECTURE.md, EXPO.md, MIGRATION.md, PERFORMANCE.md, THREAT_MODEL.md) covering architecture, Expo usage, migration notes, performance guidance, and threat model. In example/src/components/StorageCard.tsx memoize the storage options (add useMemo import) so useSecureStorage receives a stable object for silent enumeration and to avoid unnecessary re-renders. Enable declarationMap in tsconfig.json to emit declaration maps for built types.
Copilot AI review requested due to automatic review settings April 28, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the library’s developer experience and Expo compatibility by adding an Expo config plugin, strengthening TS-side validation/error reporting, and stabilizing hook return identities. It also adds extensive new documentation and standardizes formatting via .editorconfig.

Changes:

  • Add an Expo config plugin (+ tests) to configure New Architecture flags and required iOS/Android permissions.
  • Introduce TS-side input validation (InvalidArgumentError) and wire it into the core storage API.
  • Refactor hooks to reduce re-renders via structural option caching and useMemo-wrapped results; expand docs.

Reviewed changes

Copilot reviewed 31 out of 34 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tsconfig.json Enable declarationMap / exactOptionalPropertyTypes, adjust excludes.
src/internal/validate.ts Add TS-side key/service/value validation with byte-size limits.
src/internal/native.ts Lazy-load Nitro constructor and throw a friendlier error when missing (Expo Go).
src/index.ts Re-export InvalidArgumentError and isInvalidArgumentError.
src/hooks/useStableOptions.ts Replace JSON serialization caching with structural equality via deepEqual.
src/hooks/useSecurityAvailability.ts Memoize returned result object; clarify per-instance caching behavior.
src/hooks/useSecureStorage.ts Stabilize empty list identity and memoize return object; stabilize mutation options.
src/hooks/useSecureOperation.ts Memoize returned result object.
src/hooks/useSecret.ts Memoize returned result object.
src/hooks/useMutation.ts Switch to reducer-based state machine; memoize returned API.
src/hooks/useKeyRotation.ts Memoize returned result object.
src/hooks/useAsync.ts Switch to reducer-based state machine; memoize returned API; preserve data on error option.
src/hooks/types.ts Adjust optional types for exactOptionalPropertyTypes.
src/hooks/internal/deepEqual.ts Add structural equality helper used to stabilize options.
src/hooks/index.ts Re-export initial state factories.
src/errors.ts Add InvalidArgumentError + predicate and marker mapping.
src/core/storage.ts Call TS-side validators and throw typed errors from native boundary.
src/tests/internal.validate.test.ts Add unit tests for validation helpers.
src/tests/hooks.useSecret.test.tsx Adjust typings for exact optional semantics in tests.
src/tests/mocks/fixtures.ts Remove explicit value: undefined to match exact optional semantics.
package.json Add lint:fix and a Biome-only guard via prelint.
jest.config.js Expand testMatch to include root __tests__.
example/src/components/useAsyncAction.ts Remove custom async wrapper hook from example.
example/src/components/StorageCard.tsx Migrate example actions to useSecureOperation; memoize storage options.
docs/THREAT_MODEL.md Add threat model documentation.
docs/PERFORMANCE.md Add performance/bundle policy documentation.
docs/MIGRATION.md Add migration guide from 5.6 → 6.x.
docs/HOOKS.md Clarify hook caching semantics; fix import paths to /hooks.
docs/EXPO.md Add Expo integration guide and plugin options.
docs/ARCHITECTURE.md Add architecture overview and conventions.
app.plugin.js Add Expo config plugin for new arch flags, Face ID usage string, and Android permissions.
app.plugin.d.ts Add TS typings for the Expo config plugin options.
tests/app.plugin.test.ts Add unit tests for the Expo config plugin behavior/idempotency.
.editorconfig Add project-wide formatting defaults and per-filetype overrides.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/EXPO.md Outdated
Comment on lines +42 to +46
| Prop | Type | Default | Effect |
| ----------------------- | --------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `faceIDPermission` | `string` | `"Authenticate to access your secure data."` | Written to `NSFaceIDUsageDescription` if missing. A pre-existing value in your Info.plist is preserved. |
| `faceIDPermission` | `null` | — | Skips the Info.plist modifier entirely (use when another plugin owns the key). |
| `enableNewArchitecture` | `boolean` | `true` | Writes `newArchEnabled` (Android) and `RCT_NEW_ARCH_ENABLED` (iOS) flags. |

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin options table lists faceIDPermission twice (once as string and once as null). Consider merging into a single row with type string | null and describing the null behavior in the same row to avoid confusion.

Suggested change
| Prop | Type | Default | Effect |
| ----------------------- | --------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `faceIDPermission` | `string` | `"Authenticate to access your secure data."` | Written to `NSFaceIDUsageDescription` if missing. A pre-existing value in your Info.plist is preserved. |
| `faceIDPermission` | `null` || Skips the Info.plist modifier entirely (use when another plugin owns the key). |
| `enableNewArchitecture` | `boolean` | `true` | Writes `newArchEnabled` (Android) and `RCT_NEW_ARCH_ENABLED` (iOS) flags. |
| Prop | Type | Default | Effect |
| ----------------------- | ----------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `faceIDPermission` | `string \| null` | `"Authenticate to access your secure data."` | If a string is provided, it is written to `NSFaceIDUsageDescription` if missing; a pre-existing value in your Info.plist is preserved. If `null`, the Info.plist modifier is skipped entirely (use when another plugin owns the key). |
| `enableNewArchitecture` | `boolean` | `true` | Writes `newArchEnabled` (Android) and `RCT_NEW_ARCH_ENABLED` (iOS) flags. |

Copilot uses AI. Check for mistakes.
it('counts UTF-8 byte length, not character count', () => {
// '\u{1F600}' (😀) is 4 UTF-8 bytes; allow well-under-the-limit input through.
expect(() => validateValue('\u{1F600}')).not.toThrow()
})

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the surrogate-pair handling in utf8ByteLength(), it would be good to add a regression test for malformed/unpaired surrogate code units (e.g. a lone high surrogate, or a high surrogate followed by a non-low surrogate). The current implementation can miscount and skip characters in those cases, so a test would prevent reintroducing that bug once fixed.

Suggested change
})
})
it('handles a lone high surrogate without skipping or miscounting bytes', () => {
// Unpaired surrogates are encoded as the replacement character in UTF-8 (3 bytes).
const atLimit = 'x'.repeat(MAX_VALUE_BYTES - 3) + '\uD83D'
const tooBig = 'x'.repeat(MAX_VALUE_BYTES - 2) + '\uD83D'
expect(() => validateValue(atLimit)).not.toThrow()
expect(() => validateValue(tooBig)).toThrow(InvalidArgumentError)
})
it('handles a high surrogate followed by a non-low surrogate without skipping the next character', () => {
// '\uD83D' is unpaired here (3 bytes in UTF-8) and 'a' is 1 byte.
const atLimit = 'x'.repeat(MAX_VALUE_BYTES - 4) + '\uD83Da'
const tooBig = 'x'.repeat(MAX_VALUE_BYTES - 3) + '\uD83Da'
expect(() => validateValue(atLimit)).not.toThrow()
expect(() => validateValue(tooBig)).toThrow(InvalidArgumentError)
})

Copilot uses AI. Check for mistakes.
Comment thread src/internal/validate.ts Outdated
Comment on lines +21 to +31
function utf8ByteLength(input: string): number {
let bytes = 0
for (let i = 0; i < input.length; i++) {
const code = input.charCodeAt(i)
if (code < 0x80) bytes += 1
else if (code < 0x800) bytes += 2
else if (code >= 0xd800 && code <= 0xdbff) {
// High surrogate — combined with the next low surrogate is a 4-byte UTF-8 sequence.
bytes += 4
i++
} else bytes += 3

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utf8ByteLength() treats any high surrogate as the start of a surrogate pair: it always adds 4 bytes and increments i. For unpaired or malformed surrogates (high surrogate at end, or not followed by a low surrogate), this will undercount and can skip counting the next code unit entirely. Please check that the next code unit is a low surrogate before counting 4 bytes / advancing i; otherwise count the current code unit as a 3-byte sequence and continue normally.

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +45
export default function deepEqual(a: unknown, b: unknown): boolean {
if (Object.is(a, b)) return true
if (a === null || b === null) return false
if (typeof a !== 'object' || typeof b !== 'object') return false

const aIsArray = Array.isArray(a)
const bIsArray = Array.isArray(b)
if (aIsArray !== bIsArray) return false

if (aIsArray) {
const arrA = a as readonly unknown[]
const arrB = b as readonly unknown[]
if (arrA.length !== arrB.length) return false
for (let i = 0; i < arrA.length; i++) {
if (!deepEqual(arrA[i], arrB[i])) return false
}
return true
}

if (!isPlainObject(a) || !isPlainObject(b)) return false

const keysA = Object.keys(a)
const keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false

for (const key of keysA) {
if (!Object.hasOwn(b, key)) return false
if (!deepEqual(a[key], b[key])) return false
}
return true

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deepEqual() is used by hooks for stability, and useStableOptions' docs claim circular references "do not throw". As implemented, deepEqual will recurse infinitely on circular arrays/objects and eventually stack overflow. Either add cycle detection (e.g., track seen pairs) so circular structures return false safely, or update the hook/docs to explicitly state circular references are unsupported.

Copilot uses AI. Check for mistakes.
Comment thread app.plugin.js Outdated
if (faceIDPermission === null) return config
return withInfoPlist(config, (modConfig) => {
// Respect a user-set value; only fill the default when missing.
if (!modConfig.modResults.NSFaceIDUsageDescription) {

Copilot AI Apr 28, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

withFaceIDUsageDescription() checks if (!modConfig.modResults.NSFaceIDUsageDescription), which will overwrite a user-provided empty string (or other falsy values). If the intent is to only fill when the key is missing, check specifically for undefined/null instead of falsiness so existing values are always preserved.

Suggested change
if (!modConfig.modResults.NSFaceIDUsageDescription) {
if (modConfig.modResults.NSFaceIDUsageDescription == null) {

Copilot uses AI. Check for mistakes.
Respect explicitly-set Info.plist Face ID values by checking for null/undefined instead of falsiness; update docs to clarify faceIDPermission type/behavior. Fix utf8ByteLength to correctly handle surrogate pairs and unpaired high surrogates (avoid skipping/undercounting). Add unit tests for deepEqual and extend validate tests for UTF-8 edge cases. Document that deepEqual and useStableOptions do not support circular references and that non-serialisable values compare by identity.
@mCodex mCodex self-assigned this Apr 28, 2026
@mCodex mCodex merged commit 7f29e89 into master Apr 28, 2026
7 checks passed
@mCodex mCodex deleted the refactor/generalImprovements branch April 28, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants