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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ playwright.cache
temp
test-results
tmp

19 changes: 19 additions & 0 deletions platforms/javascript/react-native/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# @contentful/optimization-react-native

Contentful Optimization SDK for React Native applications.

## Installation

```bash
npm install @contentful/optimization-react-native @react-native-async-storage/async-storage
```

## Usage

```typescript
import Optimization from '@contentful/optimization-react-native'

const optimization = new Optimization({
// Your configuration
})
```
52 changes: 52 additions & 0 deletions platforms/javascript/react-native/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
{
"name": "@contentful/optimization-react-native",
"version": "1.0.0",
"license": "MIT",
"type": "commonjs",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"files": [
"dist/**/*"
],
"publishConfig": {
"directory": "dist"
},
"scripts": {
"build": "pnpm clean; pnpm build:cjs && pnpm build:esm",
"build:cjs": "tsc -p ./tsconfig.cjs.json && pnpm rename:js:cjs",
"build:esm": "tsc -p ./tsconfig.esm.json && pnpm rename:js:mjs",
"rename:js:cjs": "find ./dist -type f -name \"*.js\" ! -name \"*.cjs\" -print0 | while IFS= read -r -d '' file; do mv \"$file\" \"${file%.js}.cjs\"; done && find ./dist -type f -name \"*.js.map\" -print0 | while IFS= read -r -d '' file; do mv \"$file\" \"${file%.js.map}.cjs.map\"; done",
"rename:js:mjs": "find ./dist -type f -name \"*.js\" ! -name \"*.mjs\" -print0 | while IFS= read -r -d '' file; do mv \"$file\" \"${file%.js}.mjs\"; done && find ./dist -type f -name \"*.js.map\" -print0 | while IFS= read -r -d '' file; do mv \"$file\" \"${file%.js.map}.mjs.map\"; done",
"clean": "rimraf ./dist ./coverage tsconfig.tsbuildinfo",
"test:unit": "vitest run --coverage",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@contentful/optimization-core": "workspace:*",
"@react-native-async-storage/async-storage": "^2.1.0",
"es-toolkit": "catalog:",
"react": "^18.3.1",
"react-native": "^0.76.6",
"zod": "catalog:"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@vitest/coverage-v8": "catalog:",
"rimraf": "catalog:",
"tslib": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-native": ">=0.70.0"
}
}
36 changes: 36 additions & 0 deletions platforms/javascript/react-native/src/builders/EventBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Dimensions, Platform } from 'react-native'

export function getUserAgent(): string {
return `React Native/${Platform.Version} (${Platform.OS})`
}

export function getLocale(): string {
// React Native doesn't have built-in locale detection
// This would typically be handled by a library like react-native-localize
// For now, return a default
return 'en-US'
}

export interface PageProperties {
path: string
referrer: string
search: string
title: string
url: string
height?: number
width?: number
}

export function getPageProperties(): PageProperties {
const { width, height } = Dimensions.get('window')
// Basic implementation of "page" properties, will be decided on in NT-1692.
return {
path: '/',
referrer: '',
search: '',
title: 'React Native App',
url: 'app://',
width,
height,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { type ReactNode } from 'react'
import { Text, View } from 'react-native'

export interface OptimizationProviderProps {
children?: ReactNode
}

/**
* Placeholder component for Optimization Provider
* This is a simple stand-in component that can be imported and used in React Native applications
*/
export function OptimizationProvider({ children }: OptimizationProviderProps): React.JSX.Element {
return (
<View>
<Text>Optimization Provider Initialized</Text>
{children}
</View>
)
}

export default OptimizationProvider
33 changes: 33 additions & 0 deletions platforms/javascript/react-native/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it, vi } from 'vitest'

// Mock React Native before importing anything else
vi.mock('react-native', () => ({
Platform: { OS: 'ios' },
Dimensions: { get: vi.fn(() => ({ width: 375, height: 667 })) },
NativeModules: {},
}))

// Mock AsyncStorage
vi.mock('@react-native-async-storage/async-storage', () => ({
default: {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
},
}))

// Mock React
vi.mock('react', () => ({
default: {},
createContext: vi.fn(() => ({})),
useContext: vi.fn(),
useMemo: vi.fn(<T>(fn: () => T) => fn()),
createElement: vi.fn(),
}))

describe('Optimization React Native', () => {
it('should pass basic smoke test', () => {
// Basic smoke test to ensure the package structure is valid
expect(true).toBe(true)
})
})
82 changes: 82 additions & 0 deletions platforms/javascript/react-native/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { type CoreConfig, CoreStateful, effect, signals } from '@contentful/optimization-core'
import { merge } from 'es-toolkit'
import { getLocale, getPageProperties, getUserAgent } from './builders/EventBuilder'
import AsyncStorageStore from './storage/AsyncStorageStore'

async function mergeConfig({ defaults, logLevel, ...config }: CoreConfig): Promise<CoreConfig> {
// Initialize AsyncStorage before reading from it
await AsyncStorageStore.initialize()

return merge(
{
defaults: {
changes: AsyncStorageStore.changes ?? defaults?.changes,
consent: AsyncStorageStore.consent ?? defaults?.consent,
profile: AsyncStorageStore.profile ?? defaults?.profile,
personalizations: AsyncStorageStore.personalizations ?? defaults?.personalizations,
},
eventBuilder: {
channel: 'react-native',
library: { name: 'Optimization React Native API', version: '1.0.0' },
getLocale,
getPageProperties,
getUserAgent,
},
logLevel: AsyncStorageStore.debug ? 'debug' : logLevel,
},
config,
)
}

class Optimization extends CoreStateful {
private constructor(config: CoreConfig) {
super(config)

// Set up effects to sync state with AsyncStorage
effect(() => {
const {
changes: { value },
} = signals

AsyncStorageStore.changes = value
})

effect(() => {
const {
consent: { value },
} = signals

AsyncStorageStore.consent = value
})

effect(() => {
const {
profile: { value },
} = signals

AsyncStorageStore.profile = value

const { anonymousId: storedAnonymousId } = AsyncStorageStore

AsyncStorageStore.anonymousId = value?.id ?? storedAnonymousId
})

effect(() => {
const {
personalizations: { value },
} = signals

AsyncStorageStore.personalizations = value
})
}

static async create(config: CoreConfig): Promise<Optimization> {
const mergedConfig = await mergeConfig(config)
return new Optimization(mergedConfig)
}
}

// Export components
export { OptimizationProvider } from './components/OptimizationProvider'

export default Optimization
Loading