@tfcorp/consumer-app-react-native-sdk 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,70 @@
1
+ True Fit SDK
2
+ Copyright © 2026 True Fit Corp. All rights reserved.
3
+
4
+ TRUE FIT SDK LICENSE AGREEMENT (EULA)
5
+
6
+ This is a legal agreement between you (either an individual or an entity, "You")
7
+ and True Fit Corp. ("True Fit") governing your use of the True Fit SDK, including
8
+ all libraries, sample code, tools, and accompanying documentation (collectively,
9
+ the "SDK"). By downloading, installing, or using the SDK, You agree to be bound
10
+ by the terms of this Agreement. If You do not agree, do not download, install, or
11
+ use the SDK.
12
+
13
+ 1. PROPRIETARY SOFTWARE
14
+ The SDK is proprietary software of True Fit. It is licensed, not sold. True
15
+ Fit retains all right, title, and interest in and to the SDK, including all
16
+ intellectual property rights therein.
17
+
18
+ 2. LICENSE GRANT
19
+ Subject to your compliance with this Agreement, True Fit grants You a
20
+ non-exclusive, non-transferable, revocable, worldwide license to:
21
+ - download and install the SDK;
22
+ - use the SDK to develop applications; and
23
+ - distribute applications incorporating the SDK to your customers.
24
+
25
+ 3. RESTRICTIONS
26
+ You may not:
27
+ - redistribute the SDK itself;
28
+ - sell, sublicense, or transfer the SDK independently;
29
+ - modify, reverse engineer, decompile, or disassemble the SDK, except where
30
+ such restriction is prohibited by applicable law;
31
+ - use the SDK to create a competing SDK or service.
32
+
33
+ 4. OWNERSHIP
34
+ Except for the limited license expressly granted in Section 2, no rights,
35
+ licenses, or interests in the SDK are granted to You. All rights not expressly
36
+ granted are reserved by True Fit.
37
+
38
+ 5. TERM AND TERMINATION
39
+ This Agreement is effective until terminated. It terminates automatically,
40
+ without notice, if You breach any of its terms. Upon termination, You must
41
+ cease all use of the SDK and destroy all copies in your possession or control.
42
+ Sections 1, 3, 4, and 6 through 9 survive termination.
43
+
44
+ 6. DISCLAIMER OF WARRANTY
45
+ THE SDK IS PROVIDED "AS IS" AND "AS AVAILABLE", WITHOUT WARRANTY OF ANY KIND,
46
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF
47
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. TRUE
48
+ FIT DOES NOT WARRANT THAT THE SDK WILL BE UNINTERRUPTED OR ERROR-FREE.
49
+
50
+ 7. LIMITATION OF LIABILITY
51
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT WILL TRUE FIT
52
+ BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE
53
+ DAMAGES, OR ANY LOSS OF PROFITS, REVENUE, OR DATA, ARISING OUT OF OR RELATED
54
+ TO YOUR USE OF OR INABILITY TO USE THE SDK, WHETHER IN CONTRACT, TORT, OR
55
+ OTHERWISE, EVEN IF TRUE FIT HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
56
+ DAMAGES.
57
+
58
+ 8. GOVERNING LAW
59
+ This Agreement is governed by the laws of the Commonwealth of Massachusetts,
60
+ United States of America, without regard to its conflict-of-laws principles.
61
+ The state and federal courts located in Boston, Massachusetts have exclusive
62
+ jurisdiction over any dispute arising out of or relating to this Agreement.
63
+
64
+ 9. ENTIRE AGREEMENT
65
+ This Agreement constitutes the entire agreement between You and True Fit
66
+ regarding the SDK and supersedes all prior or contemporaneous understandings.
67
+ If any provision is held unenforceable, the remaining provisions remain in
68
+ full force and effect.
69
+
70
+ For licensing inquiries, contact: dev@truefit.com
package/README.md ADDED
@@ -0,0 +1,210 @@
1
+ # True Fit React Native WebView SDK
2
+
3
+ React Native bridge for the True Fit recommendation WebView SDK on Android and iOS.
4
+
5
+ The widget renders inline on a product page, sizes itself to its content, and
6
+ presents the size modal full screen on its own. It uses the official True Fit
7
+ `tfcapi` event contract for open/close, height, and recommendation events, and
8
+ materializes the account sign-in popup (`window.open`) so shoppers can register
9
+ and get a personalized recommendation.
10
+
11
+ This package exposes a single native view:
12
+
13
+ ```tsx
14
+ import {
15
+ TrueFitRecommendationView,
16
+ type TrueFitPresentationMode,
17
+ type TrueFitWidgetResult,
18
+ } from 'consumer-app-react-native-sdk';
19
+
20
+ export function ProductFitRecommendation() {
21
+ return (
22
+ <TrueFitRecommendationView
23
+ config={{
24
+ storeKey: 'nda',
25
+ storeStyle: '7093242789933',
26
+ }}
27
+ options={{
28
+ minWidgetHeight: 86,
29
+ maxWidgetHeight: 320,
30
+ }}
31
+ onPresentationModeChange={(mode: TrueFitPresentationMode, styleId) => {
32
+ console.log('True Fit mode', mode, styleId);
33
+ }}
34
+ onHeightChange={(height) => {
35
+ console.log('True Fit height', height);
36
+ }}
37
+ onWidgetResult={(result: TrueFitWidgetResult) => {
38
+ console.log('True Fit widget result', result);
39
+ }}
40
+ onAutoSizeSelect={({ styleId, size }) => {
41
+ console.log('True Fit auto size select', styleId, size);
42
+ }}
43
+ />
44
+ );
45
+ }
46
+ ```
47
+
48
+ ## Props
49
+
50
+ ### `config`
51
+
52
+ | Name | Type | Default |
53
+ | ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------ |
54
+ | `environment` | `'development' \| 'prod'` | `'prod'` |
55
+ | `storeKey` | `string` | Required |
56
+ | `storeStyle` | `string` | Required |
57
+ | `uiLocale` | `string` | Not set |
58
+ | `scriptUrl` | `string` | — (overrides `environment`; a full built-bundle URL. Raw Vite `.ts` dev entries are **not** supported) |
59
+
60
+ ### `options`
61
+
62
+ | Name | Type | Default |
63
+ | -------------------------------------- | --------- | ------- |
64
+ | `minWidgetHeight` | `number` | `86` |
65
+ | `maxWidgetHeight` | `number` | `320` |
66
+ | `modalBackdropOpacity` | `number` | `0.4` |
67
+ | `modalTransitionDurationMs` | `number` | `200` |
68
+ | `disableBackgroundScrollWhenModalOpen` | `boolean` | `true` |
69
+ | `heightAnimationDurationMs` | `number` | `180` |
70
+ | `debugLogging` | `boolean` | `false` |
71
+
72
+ `maxWidgetHeight` must be tall enough to fit the widest inline layout (the CTA
73
+ button plus the agentic suggestion chips row); content taller than this is
74
+ clipped by the webview.
75
+
76
+ ## Events
77
+
78
+ | Name | Payload |
79
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `onPresentationModeChange` | `(mode: 'widget' \| 'modal', styleId?: string \| null)` — `styleId` identifies which product's flow opened/closed; `null` for changes that did not originate from an open/close event |
81
+ | `onHeightChange` | Current native view height in density-independent points |
82
+ | `onWidgetResult` | `TrueFitWidgetResult` — per-style recommendation state (`styleId`, `isWidgetVisible`, `isRegistered`, `isTraveling`, `isTravelingExternal`, `successType`) |
83
+ | `onAutoSizeSelect` | `{ styleId, size }` — fires when the widget auto-selects a size for a style |
84
+
85
+ `TrueFitWidgetResult.successType` is `'personal'` when the shopper has a
86
+ profile-based recommendation, `'general'` for a generic one, and `null` when
87
+ there is none.
88
+
89
+ ## Sign-in popup
90
+
91
+ The widget's account sign-in opens the Keycloak login page via `window.open`.
92
+ The SDK materializes it natively — a `WKWebView` presented as a form sheet on
93
+ iOS, a second `WebView` in a full-screen `Dialog` on Android — sharing the same
94
+ web session (cookies) as the widget so the post-login handshake completes and
95
+ the personalized recommendation appears. No host-app wiring is required.
96
+
97
+ ## Architecture
98
+
99
+ This package is a thin adapter. It contains no WebView, modal, JS bridge,
100
+ configuration loader or resize implementation of its own — all of that lives in
101
+ the standalone True Fit native SDKs, which this package depends on:
102
+
103
+ ```text
104
+ React Native SDK (consumer-app-react-native-sdk)
105
+ ├── TypeScript public API (src/index.tsx)
106
+ ├── Android adapter (ViewManager + prop mappers) → depends on truefit-android-sdk
107
+ └── iOS adapter (RCTViewManager + prop mappers) → depends on TrueFitRecommendation pod
108
+ ```
109
+
110
+ The adapters only map props to native config, forward native callbacks to JS
111
+ events, and integrate the native view into React Native layout/lifecycle, so the
112
+ widget behaviour matches the standalone native SDK sample apps exactly.
113
+
114
+ ## Native SDK dependencies
115
+
116
+ **iOS** — the pod depends on `TrueFitRecommendation`. Your app's Podfile must
117
+ make it resolvable. For local monorepo development, point it at the SDK path:
118
+
119
+ ```ruby
120
+ # ios/Podfile
121
+ pod 'TrueFitRecommendation', :path => '../../ios/ios-sdk'
122
+ ```
123
+
124
+ Once the SDK is published, use the released version instead:
125
+
126
+ ```ruby
127
+ pod 'TrueFitRecommendation', '~> 2.0.0'
128
+ ```
129
+
130
+ **Android** — the adapter depends on the Android SDK. In the monorepo, include
131
+ the SDK as a Gradle project so the adapter picks it up via
132
+ `project(':truefit-android-sdk')`. In your app's `settings.gradle`:
133
+
134
+ ```gradle
135
+ include(':truefit-android-sdk')
136
+ project(':truefit-android-sdk').projectDir =
137
+ new File(rootProject.projectDir, '../../android/android-sdk')
138
+ ```
139
+
140
+ If that project is absent, the adapter falls back to the published artifact
141
+ `com.truefit:truefit-android-sdk:2.0.0` (add the GitHub Pages Maven repo
142
+ `https://true-fit-corp.github.io/truefit-android-maven`).
143
+
144
+ ## Integration
145
+
146
+ Install from the local workspace path:
147
+
148
+ ```sh
149
+ pnpm add file:../react-native-sdk
150
+ ```
151
+
152
+ Install iOS pods (after adding the `TrueFitRecommendation` pod line above):
153
+
154
+ ```sh
155
+ cd ios
156
+ pod install
157
+ ```
158
+
159
+ React Native autolinking should register both platforms. If your Android app
160
+ does not autolink local packages, add the package manually in
161
+ `MainApplication`:
162
+
163
+ ```kotlin
164
+ import com.truefit.reactnative.TrueFitRecommendationPackage
165
+
166
+ override fun getPackages(): List<ReactPackage> =
167
+ PackageList(this).packages.apply {
168
+ add(TrueFitRecommendationPackage())
169
+ }
170
+ ```
171
+
172
+ Use the view inside any product/detail screen. Give the parent enough width and
173
+ avoid clipping if you want the modal expansion to remain visible:
174
+
175
+ ```tsx
176
+ <View style={{ width: '100%', overflow: 'visible' }}>
177
+ <TrueFitRecommendationView
178
+ config={{
179
+ storeKey: 'your-store-key',
180
+ storeStyle: productStyleId,
181
+ }}
182
+ />
183
+ </View>
184
+ ```
185
+
186
+ Config values must be strings. If your API returns numeric product IDs, convert
187
+ them before passing (e.g. `storeStyle: String(productStyleId)`).
188
+
189
+ ## Environments
190
+
191
+ The native SDKs resolve the widget script URL from `config.environment`;
192
+ consumers never pass raw URLs. Supported values are `'prod'` (the default,
193
+ `https://consumer-v2.truefitcorp.com`) and `'development'`
194
+ (`https://consumer-dev.truefitcorp.com`, for testing); both load
195
+ `/tf-integration.js` from that origin. The SDK derives the HTML document's base URL / origin from that URL
196
+ so the module fetch, the widget's `/api` calls, and its cookies are all
197
+ same-origin.
198
+
199
+ ## Versioning & release order
200
+
201
+ The React Native adapter depends on the native SDKs, so release them first:
202
+
203
+ 1. Publish `truefit-android-sdk` (Maven coordinate `com.truefit:truefit-android-sdk`)
204
+ and `TrueFitRecommendation` (CocoaPods) at the target version.
205
+ 2. Bump the matching version constraints in `android/build.gradle` and
206
+ `TrueFitReactNativeWebViewSdk.podspec`.
207
+ 3. Publish this React Native package.
208
+
209
+ In the monorepo these steps are unnecessary — the adapter consumes the SDKs
210
+ directly via the Gradle project include and the local pod path above.
@@ -0,0 +1,28 @@
1
+ require 'json'
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = 'TrueFitReactNativeWebViewSdk'
7
+ s.version = package['version']
8
+ s.summary = package['description']
9
+ s.license = {
10
+ :type => 'Commercial',
11
+ :text => 'Copyright © 2026 True Fit Corp. All rights reserved. Use of this SDK is subject to the True Fit SDK License Agreement (EULA): https://www.truefit.com/legal/sdk-license',
12
+ }
13
+ s.authors = { 'True Fit' => 'dev@truefit.com' }
14
+ s.homepage = 'https://www.truefit.com'
15
+ s.platforms = { :ios => '13.0' }
16
+ s.source = { :path => '.' }
17
+ s.source_files = 'ios/**/*.{h,m,mm,swift}'
18
+ s.swift_version = '5.0'
19
+ s.dependency 'React-Core'
20
+
21
+ # The adapter reuses the standalone iOS SDK for all WebView / modal / bridge
22
+ # behaviour. The consuming app's Podfile must make this pod resolvable, e.g.
23
+ # for local monorepo development:
24
+ # pod 'TrueFitRecommendation', :path => '../node_modules/consumer-app-react-native-sdk/../../ios/ios-sdk'
25
+ # or, once published:
26
+ # pod 'TrueFitRecommendation', '~> 2.0.0'
27
+ s.dependency 'TrueFitRecommendation'
28
+ end
@@ -0,0 +1,52 @@
1
+ buildscript {
2
+ ext.kotlin_version = '2.0.21'
3
+ repositories {
4
+ google()
5
+ mavenCentral()
6
+ }
7
+ dependencies {
8
+ classpath 'com.android.tools.build:gradle:8.6.1'
9
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
10
+ }
11
+ }
12
+
13
+ apply plugin: 'com.android.library'
14
+ apply plugin: 'org.jetbrains.kotlin.android'
15
+
16
+ android {
17
+ namespace 'com.truefit.reactnative'
18
+ compileSdk 35
19
+
20
+ defaultConfig {
21
+ minSdk 23
22
+ }
23
+
24
+ compileOptions {
25
+ sourceCompatibility JavaVersion.VERSION_17
26
+ targetCompatibility JavaVersion.VERSION_17
27
+ }
28
+
29
+ kotlinOptions {
30
+ jvmTarget = '17'
31
+ }
32
+ }
33
+
34
+ repositories {
35
+ google()
36
+ mavenCentral()
37
+ }
38
+
39
+ dependencies {
40
+ implementation 'com.facebook.react:react-android'
41
+
42
+ // Reuse the standalone Android SDK for all WebView / modal / bridge / resize
43
+ // behaviour. In the monorepo the host app includes the SDK as a Gradle
44
+ // project (:truefit-android-sdk); otherwise the published artifact is used.
45
+ // The SDK brings its own androidx.webkit / androidx.core transitively, so the
46
+ // adapter no longer declares a WebView implementation of its own.
47
+ if (findProject(':truefit-android-sdk') != null) {
48
+ implementation project(':truefit-android-sdk')
49
+ } else {
50
+ implementation 'com.truefit:truefit-android-sdk:2.0.0'
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android" />
@@ -0,0 +1,13 @@
1
+ package com.truefit.reactnative
2
+
3
+ import com.facebook.react.ReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.uimanager.ViewManager
7
+
8
+ class TrueFitRecommendationPackage : ReactPackage {
9
+ override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = emptyList()
10
+
11
+ override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
12
+ listOf(TrueFitRecommendationViewManager())
13
+ }
@@ -0,0 +1,95 @@
1
+ package com.truefit.reactnative
2
+
3
+ import com.facebook.react.bridge.ReadableMap
4
+ import com.facebook.react.bridge.ReadableType
5
+ import com.truefit.recommendation.TrueFitEnvironment
6
+ import com.truefit.recommendation.TrueFitRecommendationConfig
7
+ import com.truefit.recommendation.TrueFitRecommendationOptions
8
+
9
+ /**
10
+ * Adapter mappers from React Native `ReadableMap` props to the standalone
11
+ * Android SDK's configuration types. The SDK owns the config shape and
12
+ * behaviour; this file only translates the JS props into it.
13
+ */
14
+ object TrueFitRecommendationProps {
15
+
16
+ /**
17
+ * Builds an SDK config from the JS `config` prop, or null when the required
18
+ * fields (storeKey, storeStyle) are absent — the adapter defers loading
19
+ * until they arrive, mirroring the SDK's required-config contract.
20
+ */
21
+ fun configOrNull(map: ReadableMap?): TrueFitRecommendationConfig? {
22
+ if (map == null) return null
23
+
24
+ val storeKey = map.stringOrNull("storeKey").orEmpty()
25
+ val storeStyle = map.stringOrNull("storeStyle").orEmpty()
26
+ if (storeKey.isBlank() || storeStyle.isBlank()) return null
27
+
28
+ return TrueFitRecommendationConfig(
29
+ storeKey = storeKey,
30
+ storeStyle = storeStyle,
31
+ environment = mapEnvironment(map.stringOrNull("environment")),
32
+ uiLocale = map.stringOrNull("uiLocale")?.takeIf { it.isNotBlank() },
33
+ userToken = map.stringOrNull("userToken")?.takeIf { it.isNotBlank() },
34
+ // A full script URL overrides `environment` (e.g. a built bundle).
35
+ // Raw Vite dev entries (.ts) are not supported by the SDK.
36
+ scriptUrlOverride = map.stringOrNull("scriptUrl")?.takeIf { it.isNotBlank() },
37
+ )
38
+ }
39
+
40
+ fun options(map: ReadableMap?): TrueFitRecommendationOptions {
41
+ val defaults = TrueFitRecommendationOptions()
42
+ if (map == null) return defaults
43
+
44
+ return TrueFitRecommendationOptions(
45
+ minWidgetHeightDp = map.intOrDefault("minWidgetHeight", defaults.minWidgetHeightDp),
46
+ maxWidgetHeightDp = map.intOrDefault("maxWidgetHeight", defaults.maxWidgetHeightDp),
47
+ modalBackdropOpacity = map.floatOrDefault("modalBackdropOpacity", defaults.modalBackdropOpacity),
48
+ modalTransitionDurationMs = map.longOrDefault(
49
+ "modalTransitionDurationMs",
50
+ defaults.modalTransitionDurationMs,
51
+ ),
52
+ disableBackgroundScrollWhenModalOpen = map.booleanOrDefault(
53
+ "disableBackgroundScrollWhenModalOpen",
54
+ defaults.disableBackgroundScrollWhenModalOpen,
55
+ ),
56
+ heightAnimationDurationMs = map.longOrDefault(
57
+ "heightAnimationDurationMs",
58
+ defaults.heightAnimationDurationMs,
59
+ ),
60
+ debugLogging = map.booleanOrDefault("debugLogging", defaults.debugLogging),
61
+ )
62
+ }
63
+
64
+ private fun mapEnvironment(name: String?): TrueFitEnvironment = when (name?.lowercase()) {
65
+ "development", "dev" -> TrueFitEnvironment.Development
66
+ "prod", "production" -> TrueFitEnvironment.Prod
67
+ else -> TrueFitEnvironment.Prod
68
+ }
69
+ }
70
+
71
+ private fun ReadableMap.stringOrNull(name: String): String? {
72
+ if (!hasKey(name) || isNull(name)) return null
73
+
74
+ return when (getType(name)) {
75
+ ReadableType.String -> getString(name)
76
+ ReadableType.Number -> {
77
+ val value = getDouble(name)
78
+ val longValue = value.toLong()
79
+ if (value == longValue.toDouble()) longValue.toString() else value.toString()
80
+ }
81
+ else -> null
82
+ }
83
+ }
84
+
85
+ private fun ReadableMap.intOrDefault(name: String, defaultValue: Int): Int =
86
+ if (hasKey(name) && !isNull(name)) getDouble(name).toInt() else defaultValue
87
+
88
+ private fun ReadableMap.longOrDefault(name: String, defaultValue: Long): Long =
89
+ if (hasKey(name) && !isNull(name)) getDouble(name).toLong() else defaultValue
90
+
91
+ private fun ReadableMap.floatOrDefault(name: String, defaultValue: Float): Float =
92
+ if (hasKey(name) && !isNull(name)) getDouble(name).toFloat() else defaultValue
93
+
94
+ private fun ReadableMap.booleanOrDefault(name: String, defaultValue: Boolean): Boolean =
95
+ if (hasKey(name) && !isNull(name)) getBoolean(name) else defaultValue
@@ -0,0 +1,52 @@
1
+ package com.truefit.reactnative
2
+
3
+ import android.content.Context
4
+ import android.view.ViewGroup
5
+ import android.widget.FrameLayout
6
+ import com.truefit.recommendation.TrueFitRecommendationView as SdkRecommendationView
7
+
8
+ /**
9
+ * Thin React Native adapter over the standalone Android SDK view
10
+ * ([SdkRecommendationView]). All WebView setup, JS bridge handling, widget and
11
+ * modal rendering, configuration, resize/height handling, navigation, cleanup
12
+ * and the sign-in popup live in the SDK; [sdkView] is instantiated directly
13
+ * from it.
14
+ *
15
+ * This wrapper adds only the React Native-specific layout integration: React
16
+ * Native (both the old and new architecture) skips measure/layout for children
17
+ * that are added or resized natively, so we force a layout pass ourselves.
18
+ * Without this the WebView can render stale or blank after the SDK reparents it
19
+ * between the inline widget and the modal overlay. Because [SdkRecommendationView]
20
+ * bubbles its `requestLayout()` up to this parent, hooking it here covers the
21
+ * reparenting the SDK does internally.
22
+ */
23
+ class TrueFitRecommendationView(context: Context) : FrameLayout(context) {
24
+
25
+ val sdkView = SdkRecommendationView(context)
26
+
27
+ init {
28
+ // The widget must not be clipped when the SDK animates its height.
29
+ clipChildren = false
30
+ clipToPadding = false
31
+ addView(
32
+ sdkView,
33
+ LayoutParams(
34
+ ViewGroup.LayoutParams.MATCH_PARENT,
35
+ ViewGroup.LayoutParams.MATCH_PARENT,
36
+ ),
37
+ )
38
+ }
39
+
40
+ private val measureAndLayout = Runnable {
41
+ measure(
42
+ MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
43
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY),
44
+ )
45
+ layout(left, top, right, bottom)
46
+ }
47
+
48
+ override fun requestLayout() {
49
+ super.requestLayout()
50
+ post(measureAndLayout)
51
+ }
52
+ }
@@ -0,0 +1,91 @@
1
+ package com.truefit.reactnative
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.ReactContext
5
+ import com.facebook.react.bridge.ReadableMap
6
+ import com.facebook.react.bridge.WritableMap
7
+ import com.facebook.react.common.MapBuilder
8
+ import com.facebook.react.uimanager.SimpleViewManager
9
+ import com.facebook.react.uimanager.ThemedReactContext
10
+ import com.facebook.react.uimanager.annotations.ReactProp
11
+ import com.facebook.react.uimanager.events.RCTEventEmitter
12
+ import com.truefit.recommendation.TrueFitRecommendationView.PresentationMode
13
+
14
+ class TrueFitRecommendationViewManager : SimpleViewManager<TrueFitRecommendationView>() {
15
+ override fun getName(): String = REACT_CLASS
16
+
17
+ override fun createViewInstance(reactContext: ThemedReactContext): TrueFitRecommendationView {
18
+ val view = TrueFitRecommendationView(reactContext)
19
+ // React Native lays out in density-independent units; the SDK reports
20
+ // widget height in device pixels, so convert px -> dp here.
21
+ val density = reactContext.resources.displayMetrics.density
22
+
23
+ view.sdkView.onPresentationModeChanged = { mode, styleId ->
24
+ val event = Arguments.createMap().apply {
25
+ putString("mode", if (mode == PresentationMode.Modal) "modal" else "widget")
26
+ if (styleId != null) putString("styleId", styleId) else putNull("styleId")
27
+ }
28
+ emitEvent(view, "topPresentationModeChange", event)
29
+ }
30
+ view.sdkView.onHeightChanged = { heightPx ->
31
+ val event = Arguments.createMap().apply {
32
+ putDouble("height", heightPx / density.toDouble())
33
+ }
34
+ emitEvent(view, "topHeightChange", event)
35
+ }
36
+ view.sdkView.onWidgetResult = { result ->
37
+ val event = Arguments.createMap().apply {
38
+ putString("styleId", result.styleId)
39
+ putBoolean("isWidgetVisible", result.isWidgetVisible)
40
+ putBoolean("isRegistered", result.isRegistered)
41
+ putBoolean("isTraveling", result.isTraveling)
42
+ putBoolean("isTravelingExternal", result.isTravelingExternal)
43
+ if (result.successType != null) putString("successType", result.successType) else putNull("successType")
44
+ }
45
+ emitEvent(view, "topWidgetResult", event)
46
+ }
47
+ view.sdkView.onAutoSizeSelect = { styleId, size ->
48
+ val event = Arguments.createMap().apply {
49
+ putString("styleId", styleId)
50
+ putString("size", size)
51
+ }
52
+ emitEvent(view, "topAutoSizeSelect", event)
53
+ }
54
+ return view
55
+ }
56
+
57
+ @ReactProp(name = "config")
58
+ fun setConfig(view: TrueFitRecommendationView, config: ReadableMap?) {
59
+ // Config props arrive incrementally; only load once the required fields
60
+ // are present. The SDK guards against reloading an unchanged config.
61
+ TrueFitRecommendationProps.configOrNull(config)?.let(view.sdkView::load)
62
+ }
63
+
64
+ @ReactProp(name = "options")
65
+ fun setOptions(view: TrueFitRecommendationView, options: ReadableMap?) {
66
+ view.sdkView.options = TrueFitRecommendationProps.options(options)
67
+ }
68
+
69
+ override fun onDropViewInstance(view: TrueFitRecommendationView) {
70
+ view.sdkView.dispose()
71
+ super.onDropViewInstance(view)
72
+ }
73
+
74
+ override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> = hashMapOf(
75
+ "topPresentationModeChange" to MapBuilder.of("registrationName", "onPresentationModeChange"),
76
+ "topHeightChange" to MapBuilder.of("registrationName", "onHeightChange"),
77
+ "topWidgetResult" to MapBuilder.of("registrationName", "onWidgetResult"),
78
+ "topAutoSizeSelect" to MapBuilder.of("registrationName", "onAutoSizeSelect"),
79
+ )
80
+
81
+ private fun emitEvent(view: TrueFitRecommendationView, eventName: String, payload: WritableMap) {
82
+ val reactContext = view.context as? ReactContext ?: return
83
+ reactContext
84
+ .getJSModule(RCTEventEmitter::class.java)
85
+ .receiveEvent(view.id, eventName, payload)
86
+ }
87
+
88
+ companion object {
89
+ const val REACT_CLASS = "TrueFitRecommendationView"
90
+ }
91
+ }
@@ -0,0 +1,186 @@
1
+ import React
2
+ import TrueFitRecommendation
3
+ import UIKit
4
+
5
+ // Thin React Native adapter over the standalone iOS SDK
6
+ // (`TrueFitRecommendation`). All WebView setup, JS bridge handling, widget and
7
+ // modal rendering, configuration, resize/height handling, navigation, cleanup
8
+ // and the sign-in popup live in the SDK. This class only:
9
+ // - maps React Native `NSDictionary` props to the SDK config/options types,
10
+ // - forwards the SDK's callbacks to React Native `RCTDirectEventBlock`s,
11
+ // - integrates the SDK view into React Native's layout/lifecycle.
12
+ //
13
+ // The class is named distinctly from the SDK's `TrueFitRecommendationView`; the
14
+ // JS component name ("TrueFitRecommendationView") is derived from the view
15
+ // manager, not this class, so the public API is unchanged.
16
+ @objc(TrueFitReactNativeView)
17
+ final class TrueFitReactNativeView: UIView {
18
+ private let recommendationView = TrueFitRecommendationView()
19
+
20
+ // Event callbacks bridged to React Native (see the view manager). styleId
21
+ // identifies which product's flow changed; nil when the change did not
22
+ // originate from an open/close event.
23
+ @objc var onPresentationModeChange: RCTDirectEventBlock?
24
+ @objc var onHeightChange: RCTDirectEventBlock?
25
+ @objc var onWidgetResult: RCTDirectEventBlock?
26
+ @objc var onAutoSizeSelect: RCTDirectEventBlock?
27
+
28
+ @objc var config: NSDictionary? {
29
+ didSet { applyConfig() }
30
+ }
31
+
32
+ // Remapped from the JS `options` prop (see the .m) to avoid colliding with
33
+ // UIView semantics; maps to the SDK's `TrueFitRecommendationOptions`.
34
+ @objc var reactOptions: NSDictionary? {
35
+ didSet {
36
+ recommendationView.options = Self.options(from: reactOptions)
37
+ // Options changed (e.g. min/max height); re-map so a still-valid
38
+ // config reloads through the SDK if needed.
39
+ applyConfig()
40
+ }
41
+ }
42
+
43
+ override init(frame: CGRect) {
44
+ super.init(frame: frame)
45
+ setup()
46
+ }
47
+
48
+ required init?(coder: NSCoder) {
49
+ super.init(coder: coder)
50
+ setup()
51
+ }
52
+
53
+ // The adapter is not part of the SDK view's internal retain cycle
54
+ // (webView configuration -> message handler -> SDK view), so it deinits
55
+ // when React Native releases it on unmount. Disposing here removes the
56
+ // message handler, breaking that cycle so the SDK view is released too.
57
+ deinit {
58
+ recommendationView.dispose()
59
+ }
60
+
61
+ private func setup() {
62
+ backgroundColor = .clear
63
+ clipsToBounds = false
64
+
65
+ recommendationView.translatesAutoresizingMaskIntoConstraints = false
66
+ addSubview(recommendationView)
67
+ // The SDK view self-sizes and reports its height; React Native drives
68
+ // this container's height from onHeightChange. Pin the SDK view to fill
69
+ // the container so it tracks the React Native-provided frame.
70
+ NSLayoutConstraint.activate([
71
+ recommendationView.leadingAnchor.constraint(equalTo: leadingAnchor),
72
+ recommendationView.trailingAnchor.constraint(equalTo: trailingAnchor),
73
+ recommendationView.topAnchor.constraint(equalTo: topAnchor),
74
+ recommendationView.bottomAnchor.constraint(equalTo: bottomAnchor),
75
+ ])
76
+
77
+ recommendationView.onPresentationModeChanged = { [weak self] mode, styleId in
78
+ self?.onPresentationModeChange?([
79
+ "mode": mode == .modal ? "modal" : "widget",
80
+ "styleId": styleId ?? NSNull(),
81
+ ])
82
+ }
83
+ recommendationView.onHeightChanged = { [weak self] height in
84
+ // iOS measurements are already in points, which are React Native's
85
+ // layout units — no conversion required.
86
+ self?.onHeightChange?(["height": height])
87
+ }
88
+ recommendationView.onWidgetResult = { [weak self] result in
89
+ self?.onWidgetResult?([
90
+ "styleId": result.styleId,
91
+ "isWidgetVisible": result.isWidgetVisible,
92
+ "isRegistered": result.isRegistered,
93
+ "isTraveling": result.isTraveling,
94
+ "isTravelingExternal": result.isTravelingExternal,
95
+ "successType": result.successType ?? NSNull(),
96
+ ])
97
+ }
98
+ recommendationView.onAutoSizeSelect = { [weak self] styleId, size in
99
+ self?.onAutoSizeSelect?(["styleId": styleId, "size": size])
100
+ }
101
+ }
102
+
103
+ private func applyConfig() {
104
+ // Config props arrive incrementally; only load once the required
105
+ // fields are present, mirroring the SDK's required-config contract.
106
+ guard let config,
107
+ let mapped = Self.config(from: config) else { return }
108
+ recommendationView.load(mapped)
109
+ }
110
+
111
+ // MARK: - Prop mapping
112
+
113
+ private static func config(from dictionary: NSDictionary) -> TrueFitRecommendationConfig? {
114
+ guard let storeKey = string(dictionary["storeKey"]), !storeKey.isEmpty,
115
+ let storeStyle = string(dictionary["storeStyle"]), !storeStyle.isEmpty
116
+ else {
117
+ return nil
118
+ }
119
+
120
+ let environment = string(dictionary["environment"]).flatMap(mapEnvironment) ?? .prod
121
+ let uiLocale = string(dictionary["uiLocale"])?.nonEmpty
122
+ let userToken = string(dictionary["userToken"])?.nonEmpty
123
+ // A full script URL overrides `environment` (e.g. a built bundle). Raw
124
+ // Vite dev entries (.ts) are not supported by the SDK.
125
+ let scriptUrlOverride = string(dictionary["scriptUrl"])?.nonEmpty
126
+
127
+ return TrueFitRecommendationConfig(
128
+ storeKey: storeKey,
129
+ storeStyle: storeStyle,
130
+ environment: environment,
131
+ uiLocale: uiLocale,
132
+ userToken: userToken,
133
+ scriptUrlOverride: scriptUrlOverride
134
+ )
135
+ }
136
+
137
+ private static func mapEnvironment(_ name: String) -> TrueFitEnvironment {
138
+ switch name.lowercased() {
139
+ case "development", "dev":
140
+ .development
141
+ case "prod", "production":
142
+ .prod
143
+ default:
144
+ .prod
145
+ }
146
+ }
147
+
148
+ private static func options(from dictionary: NSDictionary?) -> TrueFitRecommendationOptions {
149
+ var options = TrueFitRecommendationOptions()
150
+ guard let dictionary else { return options }
151
+
152
+ if let value = dictionary["minWidgetHeight"] as? NSNumber {
153
+ options.minWidgetHeight = CGFloat(value.doubleValue)
154
+ }
155
+ if let value = dictionary["maxWidgetHeight"] as? NSNumber {
156
+ options.maxWidgetHeight = CGFloat(value.doubleValue)
157
+ }
158
+ if let value = dictionary["modalBackdropOpacity"] as? NSNumber {
159
+ options.modalBackdropOpacity = CGFloat(value.doubleValue)
160
+ }
161
+ if let durationMs = dictionary["modalTransitionDurationMs"] as? NSNumber {
162
+ options.modalTransitionDuration = durationMs.doubleValue / 1000
163
+ }
164
+ if let value = dictionary["disableBackgroundScrollWhenModalOpen"] as? Bool {
165
+ options.disableBackgroundScrollWhenModalOpen = value
166
+ }
167
+ if let durationMs = dictionary["heightAnimationDurationMs"] as? NSNumber {
168
+ options.heightAnimationDuration = durationMs.doubleValue / 1000
169
+ }
170
+ if let value = dictionary["debugLogging"] as? Bool {
171
+ options.debugLogging = value
172
+ }
173
+
174
+ return options
175
+ }
176
+
177
+ private static func string(_ value: Any?) -> String? {
178
+ if let string = value as? String { return string }
179
+ if let number = value as? NSNumber { return number.stringValue }
180
+ return nil
181
+ }
182
+ }
183
+
184
+ private extension String {
185
+ var nonEmpty: String? { isEmpty ? nil : self }
186
+ }
@@ -0,0 +1,12 @@
1
+ #import <React/RCTViewManager.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(TrueFitRecommendationViewManager, RCTViewManager)
4
+
5
+ RCT_EXPORT_VIEW_PROPERTY(config, NSDictionary)
6
+ RCT_REMAP_VIEW_PROPERTY(options, reactOptions, NSDictionary)
7
+ RCT_EXPORT_VIEW_PROPERTY(onPresentationModeChange, RCTDirectEventBlock)
8
+ RCT_EXPORT_VIEW_PROPERTY(onHeightChange, RCTDirectEventBlock)
9
+ RCT_EXPORT_VIEW_PROPERTY(onWidgetResult, RCTDirectEventBlock)
10
+ RCT_EXPORT_VIEW_PROPERTY(onAutoSizeSelect, RCTDirectEventBlock)
11
+
12
+ @end
@@ -0,0 +1,12 @@
1
+ import React
2
+
3
+ @objc(TrueFitRecommendationViewManager)
4
+ final class TrueFitRecommendationViewManager: RCTViewManager {
5
+ override static func requiresMainQueueSetup() -> Bool {
6
+ true
7
+ }
8
+
9
+ override func view() -> UIView! {
10
+ TrueFitReactNativeView()
11
+ }
12
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.TrueFitRecommendationView = TrueFitRecommendationView;
7
+ exports.default = void 0;
8
+ var _react = _interopRequireWildcard(require("react"));
9
+ var _reactNative = require("react-native");
10
+ var _jsxRuntime = require("react/jsx-runtime");
11
+ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
12
+ /**
13
+ * Which True Fit environment to point at. The native SDKs resolve the widget
14
+ * script URL from this internally; consumers never pass raw URLs.
15
+ */
16
+
17
+ /**
18
+ * Per-style recommendation state emitted by the widget's `widgetResult` event.
19
+ * `successType` is `'personal'` when the shopper has a profile-based
20
+ * recommendation, `'general'` for a generic one, and `null` when there is none.
21
+ */
22
+
23
+ const NativeTrueFitRecommendationView = (0, _reactNative.requireNativeComponent)('TrueFitRecommendationView');
24
+ const DEFAULT_MIN_HEIGHT = 86;
25
+ function TrueFitRecommendationView({
26
+ config,
27
+ options,
28
+ style,
29
+ onPresentationModeChange,
30
+ onHeightChange,
31
+ onWidgetResult,
32
+ onAutoSizeSelect
33
+ }) {
34
+ const minHeight = options?.minWidgetHeight ?? DEFAULT_MIN_HEIGHT;
35
+ const [measuredHeight, setMeasuredHeight] = (0, _react.useState)(minHeight);
36
+ const [presentationMode, setPresentationMode] = (0, _react.useState)('widget');
37
+ const handleHeightChange = (0, _react.useCallback)(event => {
38
+ const nextHeight = Math.max(event.nativeEvent.height, minHeight);
39
+ setMeasuredHeight(nextHeight);
40
+ onHeightChange?.(nextHeight);
41
+ }, [minHeight, onHeightChange]);
42
+ const handlePresentationModeChange = (0, _react.useCallback)(event => {
43
+ const nextMode = event.nativeEvent.mode;
44
+ setPresentationMode(nextMode);
45
+ onPresentationModeChange?.(nextMode, event.nativeEvent.styleId ?? null);
46
+ }, [onPresentationModeChange]);
47
+ const handleWidgetResult = (0, _react.useCallback)(event => {
48
+ onWidgetResult?.(event.nativeEvent);
49
+ }, [onWidgetResult]);
50
+ const handleAutoSizeSelect = (0, _react.useCallback)(event => {
51
+ onAutoSizeSelect?.(event.nativeEvent);
52
+ }, [onAutoSizeSelect]);
53
+ const containerStyle = (0, _react.useMemo)(() => [styles.container, style, {
54
+ height: measuredHeight,
55
+ minHeight,
56
+ zIndex: presentationMode === 'modal' ? 1000 : undefined,
57
+ elevation: presentationMode === 'modal' ? 16 : undefined
58
+ }], [measuredHeight, minHeight, presentationMode, style]);
59
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
60
+ style: containerStyle,
61
+ collapsable: false,
62
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(NativeTrueFitRecommendationView, {
63
+ config: config,
64
+ options: options,
65
+ style: styles.nativeView,
66
+ onHeightChange: handleHeightChange,
67
+ onPresentationModeChange: handlePresentationModeChange,
68
+ onWidgetResult: handleWidgetResult,
69
+ onAutoSizeSelect: handleAutoSizeSelect
70
+ })
71
+ });
72
+ }
73
+ const styles = _reactNative.StyleSheet.create({
74
+ container: {
75
+ width: '100%',
76
+ overflow: 'visible',
77
+ position: 'relative'
78
+ },
79
+ nativeView: {
80
+ width: '100%',
81
+ height: '100%'
82
+ }
83
+ });
84
+ var _default = exports.default = TrueFitRecommendationView;
85
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_react","_interopRequireWildcard","require","_reactNative","_jsxRuntime","e","t","WeakMap","r","n","__esModule","o","i","f","__proto__","default","has","get","set","hasOwnProperty","call","Object","defineProperty","getOwnPropertyDescriptor","NativeTrueFitRecommendationView","requireNativeComponent","DEFAULT_MIN_HEIGHT","TrueFitRecommendationView","config","options","style","onPresentationModeChange","onHeightChange","onWidgetResult","onAutoSizeSelect","minHeight","minWidgetHeight","measuredHeight","setMeasuredHeight","useState","presentationMode","setPresentationMode","handleHeightChange","useCallback","event","nextHeight","Math","max","nativeEvent","height","handlePresentationModeChange","nextMode","mode","styleId","handleWidgetResult","handleAutoSizeSelect","containerStyle","useMemo","styles","container","zIndex","undefined","elevation","jsx","View","collapsable","children","nativeView","StyleSheet","create","width","overflow","position","_default","exports"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"commonjs"}
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { requireNativeComponent, StyleSheet, View } from 'react-native';
5
+
6
+ /**
7
+ * Which True Fit environment to point at. The native SDKs resolve the widget
8
+ * script URL from this internally; consumers never pass raw URLs.
9
+ */
10
+
11
+ /**
12
+ * Per-style recommendation state emitted by the widget's `widgetResult` event.
13
+ * `successType` is `'personal'` when the shopper has a profile-based
14
+ * recommendation, `'general'` for a generic one, and `null` when there is none.
15
+ */
16
+ import { jsx as _jsx } from "react/jsx-runtime";
17
+ const NativeTrueFitRecommendationView = requireNativeComponent('TrueFitRecommendationView');
18
+ const DEFAULT_MIN_HEIGHT = 86;
19
+ export function TrueFitRecommendationView({
20
+ config,
21
+ options,
22
+ style,
23
+ onPresentationModeChange,
24
+ onHeightChange,
25
+ onWidgetResult,
26
+ onAutoSizeSelect
27
+ }) {
28
+ const minHeight = options?.minWidgetHeight ?? DEFAULT_MIN_HEIGHT;
29
+ const [measuredHeight, setMeasuredHeight] = useState(minHeight);
30
+ const [presentationMode, setPresentationMode] = useState('widget');
31
+ const handleHeightChange = useCallback(event => {
32
+ const nextHeight = Math.max(event.nativeEvent.height, minHeight);
33
+ setMeasuredHeight(nextHeight);
34
+ onHeightChange?.(nextHeight);
35
+ }, [minHeight, onHeightChange]);
36
+ const handlePresentationModeChange = useCallback(event => {
37
+ const nextMode = event.nativeEvent.mode;
38
+ setPresentationMode(nextMode);
39
+ onPresentationModeChange?.(nextMode, event.nativeEvent.styleId ?? null);
40
+ }, [onPresentationModeChange]);
41
+ const handleWidgetResult = useCallback(event => {
42
+ onWidgetResult?.(event.nativeEvent);
43
+ }, [onWidgetResult]);
44
+ const handleAutoSizeSelect = useCallback(event => {
45
+ onAutoSizeSelect?.(event.nativeEvent);
46
+ }, [onAutoSizeSelect]);
47
+ const containerStyle = useMemo(() => [styles.container, style, {
48
+ height: measuredHeight,
49
+ minHeight,
50
+ zIndex: presentationMode === 'modal' ? 1000 : undefined,
51
+ elevation: presentationMode === 'modal' ? 16 : undefined
52
+ }], [measuredHeight, minHeight, presentationMode, style]);
53
+ return /*#__PURE__*/_jsx(View, {
54
+ style: containerStyle,
55
+ collapsable: false,
56
+ children: /*#__PURE__*/_jsx(NativeTrueFitRecommendationView, {
57
+ config: config,
58
+ options: options,
59
+ style: styles.nativeView,
60
+ onHeightChange: handleHeightChange,
61
+ onPresentationModeChange: handlePresentationModeChange,
62
+ onWidgetResult: handleWidgetResult,
63
+ onAutoSizeSelect: handleAutoSizeSelect
64
+ })
65
+ });
66
+ }
67
+ const styles = StyleSheet.create({
68
+ container: {
69
+ width: '100%',
70
+ overflow: 'visible',
71
+ position: 'relative'
72
+ },
73
+ nativeView: {
74
+ width: '100%',
75
+ height: '100%'
76
+ }
77
+ });
78
+ export default TrueFitRecommendationView;
79
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["React","useCallback","useMemo","useState","requireNativeComponent","StyleSheet","View","jsx","_jsx","NativeTrueFitRecommendationView","DEFAULT_MIN_HEIGHT","TrueFitRecommendationView","config","options","style","onPresentationModeChange","onHeightChange","onWidgetResult","onAutoSizeSelect","minHeight","minWidgetHeight","measuredHeight","setMeasuredHeight","presentationMode","setPresentationMode","handleHeightChange","event","nextHeight","Math","max","nativeEvent","height","handlePresentationModeChange","nextMode","mode","styleId","handleWidgetResult","handleAutoSizeSelect","containerStyle","styles","container","zIndex","undefined","elevation","collapsable","children","nativeView","create","width","overflow","position"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { StyleProp, ViewStyle } from 'react-native';
3
+ /**
4
+ * Which True Fit environment to point at. The native SDKs resolve the widget
5
+ * script URL from this internally; consumers never pass raw URLs.
6
+ */
7
+ export type TrueFitEnvironment = 'development' | 'prod';
8
+ export type TrueFitRecommendationConfig = {
9
+ environment?: TrueFitEnvironment;
10
+ storeKey?: string;
11
+ storeStyle?: string;
12
+ uiLocale?: string;
13
+ /** Connect user token to seed into the Android widget cookie store. */
14
+ userToken?: string;
15
+ /**
16
+ * Full widget script URL that overrides `environment` (e.g. a built bundle).
17
+ * The WebView origin is derived from it. Raw Vite dev entries (`.ts`) are not
18
+ * supported.
19
+ */
20
+ scriptUrl?: string;
21
+ };
22
+ export type TrueFitRecommendationOptions = {
23
+ minWidgetHeight?: number;
24
+ maxWidgetHeight?: number;
25
+ modalBackdropOpacity?: number;
26
+ modalTransitionDurationMs?: number;
27
+ disableBackgroundScrollWhenModalOpen?: boolean;
28
+ heightAnimationDurationMs?: number;
29
+ debugLogging?: boolean;
30
+ };
31
+ export type TrueFitPresentationMode = 'widget' | 'modal';
32
+ /**
33
+ * Per-style recommendation state emitted by the widget's `widgetResult` event.
34
+ * `successType` is `'personal'` when the shopper has a profile-based
35
+ * recommendation, `'general'` for a generic one, and `null` when there is none.
36
+ */
37
+ export type TrueFitWidgetResult = {
38
+ styleId: string;
39
+ isWidgetVisible: boolean;
40
+ isRegistered: boolean;
41
+ isTraveling: boolean;
42
+ isTravelingExternal: boolean;
43
+ successType: string | null;
44
+ };
45
+ export type TrueFitAutoSizeSelect = {
46
+ styleId: string;
47
+ size: string;
48
+ };
49
+ export type TrueFitRecommendationViewProps = {
50
+ config?: TrueFitRecommendationConfig;
51
+ options?: TrueFitRecommendationOptions;
52
+ style?: StyleProp<ViewStyle>;
53
+ onPresentationModeChange?: (mode: TrueFitPresentationMode, styleId?: string | null) => void;
54
+ onHeightChange?: (height: number) => void;
55
+ onWidgetResult?: (result: TrueFitWidgetResult) => void;
56
+ onAutoSizeSelect?: (selection: TrueFitAutoSizeSelect) => void;
57
+ };
58
+ export declare function TrueFitRecommendationView({ config, options, style, onPresentationModeChange, onHeightChange, onWidgetResult, onAutoSizeSelect, }: TrueFitRecommendationViewProps): React.JSX.Element;
59
+ export default TrueFitRecommendationView;
60
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAC9D,OAAO,EAGL,SAAS,EAGT,SAAS,EACV,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,2BAA2B,GAAG;IACxC,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uEAAuE;IACvE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,oCAAoC,CAAC,EAAE,OAAO,CAAC;IAC/C,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEzD;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,OAAO,CAAC;IACrB,mBAAmB,EAAE,OAAO,CAAC;IAC7B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAqBF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,OAAO,CAAC,EAAE,4BAA4B,CAAC;IACvC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,wBAAwB,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5F,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACvD,gBAAgB,CAAC,EAAE,CAAC,SAAS,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAC/D,CAAC;AAQF,wBAAgB,yBAAyB,CAAC,EACxC,MAAM,EACN,OAAO,EACP,KAAK,EACL,wBAAwB,EACxB,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,EAAE,8BAA8B,qBAgEhC;AAcD,eAAe,yBAAyB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@tfcorp/consumer-app-react-native-sdk",
3
+ "version": "2.0.0-beta.1",
4
+ "description": "React Native bridge for the True Fit recommendation WebView SDK.",
5
+ "main": "lib/commonjs/index.js",
6
+ "module": "lib/module/index.js",
7
+ "types": "lib/typescript/index.d.ts",
8
+ "react-native": "lib/module/index.js",
9
+ "source": "src/index.tsx",
10
+ "homepage": "https://www.truefit.com",
11
+ "author": "True Fit <dev@truefit.com>",
12
+ "license": "SEE LICENSE IN LICENSE",
13
+ "files": [
14
+ "lib",
15
+ "android",
16
+ "ios",
17
+ "*.podspec",
18
+ "react-native.config.js",
19
+ "!**/build",
20
+ "!**/__tests__",
21
+ "!**/.gradle",
22
+ "!**/*.tsbuildinfo"
23
+ ],
24
+ "peerDependencies": {
25
+ "react": "*",
26
+ "react-native": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@types/react": "^18.2.0",
30
+ "del-cli": "^5.1.0",
31
+ "react": "18.2.0",
32
+ "react-native": "0.74.5",
33
+ "react-native-builder-bob": "^0.30.0",
34
+ "typescript": "^5.0.0"
35
+ },
36
+ "react-native-builder-bob": {
37
+ "source": "src",
38
+ "output": "lib",
39
+ "targets": [
40
+ "commonjs",
41
+ "module",
42
+ "typescript"
43
+ ]
44
+ },
45
+ "scripts": {
46
+ "typecheck": "tsc --noEmit",
47
+ "clean": "del-cli lib",
48
+ "build": "bob build"
49
+ }
50
+ }
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ sourceDir: './android',
6
+ },
7
+ ios: {
8
+ podspecPath: './TrueFitReactNativeWebViewSdk.podspec',
9
+ },
10
+ },
11
+ },
12
+ };