@schematichq/schematic-react 1.2.10 → 1.2.12
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/README.md +37 -0
- package/dist/schematic-react.cjs.js +127 -3
- package/dist/schematic-react.d.ts +1 -1
- package/dist/schematic-react.esm.js +127 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -136,6 +136,43 @@ const MyComponent = () => {
|
|
|
136
136
|
|
|
137
137
|
*Note: `useSchematicIsPending` is checking if entitlement data has been loaded, typically via `identify`. It should, therefore, be used to wrap flag and entitlement checks, but never the initial call to `identify`.*
|
|
138
138
|
|
|
139
|
+
## React Native
|
|
140
|
+
|
|
141
|
+
### Handling app background/foreground
|
|
142
|
+
|
|
143
|
+
When a React Native app is backgrounded for an extended period, the WebSocket connection may be closed by the OS. When the app returns to the foreground, the connection will automatically reconnect, but this happens on an exponential backoff timer which may cause a delay before fresh flag values are available.
|
|
144
|
+
|
|
145
|
+
For cases where you need immediate flag updates when returning to the foreground (e.g., after an in-app purchase), you can use one of these methods to re-establish the connection:
|
|
146
|
+
|
|
147
|
+
- `forceReconnect()`: Always closes and re-establishes the WebSocket connection, guaranteeing fresh values
|
|
148
|
+
- `reconnectIfNeeded()`: Only reconnects if the current connection is unhealthy (more efficient for frequent foreground events)
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import { useSchematic } from "@schematichq/schematic-react";
|
|
152
|
+
import { useEffect } from "react";
|
|
153
|
+
import { AppState } from "react-native";
|
|
154
|
+
|
|
155
|
+
const SchematicAppStateHandler = () => {
|
|
156
|
+
const { client } = useSchematic();
|
|
157
|
+
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
const subscription = AppState.addEventListener("change", (state) => {
|
|
160
|
+
if (state === "active") {
|
|
161
|
+
// Use forceReconnect() for guaranteed fresh values
|
|
162
|
+
client.forceReconnect();
|
|
163
|
+
// Or use reconnectIfNeeded() to skip if connection is healthy
|
|
164
|
+
// client.reconnectIfNeeded();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return () => subscription.remove();
|
|
168
|
+
}, [client]);
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
};
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Render this inside your `SchematicProvider`.
|
|
175
|
+
|
|
139
176
|
## Troubleshooting
|
|
140
177
|
|
|
141
178
|
For debugging and development, Schematic supports two special modes:
|
|
@@ -799,7 +799,7 @@ function contextString(context) {
|
|
|
799
799
|
}, {});
|
|
800
800
|
return JSON.stringify(sortedContext);
|
|
801
801
|
}
|
|
802
|
-
var version = "1.2.
|
|
802
|
+
var version = "1.2.12";
|
|
803
803
|
var anonymousIdKey = "schematicId";
|
|
804
804
|
var Schematic = class {
|
|
805
805
|
additionalHeaders = {};
|
|
@@ -1610,6 +1610,130 @@ var Schematic = class {
|
|
|
1610
1610
|
/**
|
|
1611
1611
|
* Websocket management
|
|
1612
1612
|
*/
|
|
1613
|
+
/**
|
|
1614
|
+
* Force an immediate WebSocket reconnection.
|
|
1615
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1616
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1617
|
+
* rather than waiting for the exponential backoff timer.
|
|
1618
|
+
*
|
|
1619
|
+
* This method will:
|
|
1620
|
+
* - Cancel any pending reconnection timer
|
|
1621
|
+
* - Reset the reconnection attempt counter
|
|
1622
|
+
* - Close any existing connection
|
|
1623
|
+
* - Immediately attempt to reconnect
|
|
1624
|
+
* - Re-send the current context to get fresh flag values
|
|
1625
|
+
*
|
|
1626
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1627
|
+
*
|
|
1628
|
+
* @example
|
|
1629
|
+
* ```typescript
|
|
1630
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1631
|
+
* useEffect(() => {
|
|
1632
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1633
|
+
* if (state === "active") {
|
|
1634
|
+
* client.forceReconnect();
|
|
1635
|
+
* }
|
|
1636
|
+
* });
|
|
1637
|
+
* return () => subscription.remove();
|
|
1638
|
+
* }, [client]);
|
|
1639
|
+
* ```
|
|
1640
|
+
*/
|
|
1641
|
+
forceReconnect = async () => {
|
|
1642
|
+
return this.reconnect({ force: true });
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1646
|
+
* This is useful when the application returns from a background state and wants to
|
|
1647
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1648
|
+
* the connection is still active.
|
|
1649
|
+
*
|
|
1650
|
+
* This method will:
|
|
1651
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1652
|
+
* - If healthy, return immediately without reconnecting
|
|
1653
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1654
|
+
*
|
|
1655
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1656
|
+
*
|
|
1657
|
+
* @example
|
|
1658
|
+
* ```typescript
|
|
1659
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1660
|
+
* useEffect(() => {
|
|
1661
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1662
|
+
* if (state === "active") {
|
|
1663
|
+
* client.reconnectIfNeeded();
|
|
1664
|
+
* }
|
|
1665
|
+
* });
|
|
1666
|
+
* return () => subscription.remove();
|
|
1667
|
+
* }, [client]);
|
|
1668
|
+
* ```
|
|
1669
|
+
*/
|
|
1670
|
+
reconnectIfNeeded = async () => {
|
|
1671
|
+
return this.reconnect({ force: false });
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1674
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1675
|
+
*/
|
|
1676
|
+
reconnect = async (options) => {
|
|
1677
|
+
const { force } = options;
|
|
1678
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1679
|
+
if (this.isOffline()) {
|
|
1680
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1681
|
+
return Promise.resolve();
|
|
1682
|
+
}
|
|
1683
|
+
if (!force && this.conn !== null) {
|
|
1684
|
+
try {
|
|
1685
|
+
const existingSocket = await this.conn;
|
|
1686
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1687
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1688
|
+
return Promise.resolve();
|
|
1689
|
+
}
|
|
1690
|
+
} catch {
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
this.debug(
|
|
1694
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1695
|
+
);
|
|
1696
|
+
this.wsIntentionalDisconnect = false;
|
|
1697
|
+
if (this.wsReconnectTimer !== null) {
|
|
1698
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1699
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1700
|
+
this.wsReconnectTimer = null;
|
|
1701
|
+
}
|
|
1702
|
+
this.wsReconnectAttempts = 0;
|
|
1703
|
+
if (this.conn !== null) {
|
|
1704
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1705
|
+
try {
|
|
1706
|
+
const existingSocket = await this.conn;
|
|
1707
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1708
|
+
this.currentWebSocket = null;
|
|
1709
|
+
}
|
|
1710
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1711
|
+
existingSocket.close();
|
|
1712
|
+
}
|
|
1713
|
+
} catch (error) {
|
|
1714
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1715
|
+
}
|
|
1716
|
+
this.conn = null;
|
|
1717
|
+
this.isConnecting = false;
|
|
1718
|
+
}
|
|
1719
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1720
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1721
|
+
try {
|
|
1722
|
+
this.isConnecting = true;
|
|
1723
|
+
this.conn = this.wsConnect();
|
|
1724
|
+
const socket = await this.conn;
|
|
1725
|
+
this.isConnecting = false;
|
|
1726
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1727
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1728
|
+
} catch (error) {
|
|
1729
|
+
this.isConnecting = false;
|
|
1730
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1731
|
+
}
|
|
1732
|
+
} else {
|
|
1733
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1734
|
+
}
|
|
1735
|
+
return Promise.resolve();
|
|
1736
|
+
};
|
|
1613
1737
|
/**
|
|
1614
1738
|
* If using websocket mode, close the connection when done.
|
|
1615
1739
|
* In offline mode, this is a no-op.
|
|
@@ -2013,7 +2137,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
2013
2137
|
var import_react = __toESM(require("react"));
|
|
2014
2138
|
|
|
2015
2139
|
// src/version.ts
|
|
2016
|
-
var version2 = "1.2.
|
|
2140
|
+
var version2 = "1.2.12";
|
|
2017
2141
|
|
|
2018
2142
|
// src/context/schematic.tsx
|
|
2019
2143
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
@@ -2028,7 +2152,7 @@ var SchematicProvider = ({
|
|
|
2028
2152
|
}) => {
|
|
2029
2153
|
const initialOptsRef = (0, import_react.useRef)({
|
|
2030
2154
|
publishableKey,
|
|
2031
|
-
useWebSocket:
|
|
2155
|
+
useWebSocket: true,
|
|
2032
2156
|
additionalHeaders: {
|
|
2033
2157
|
"X-Schematic-Client-Version": `schematic-react@${version2}`
|
|
2034
2158
|
},
|
|
@@ -15,7 +15,7 @@ import { StoragePersister } from '@schematichq/schematic-js';
|
|
|
15
15
|
import { Traits } from '@schematichq/schematic-js';
|
|
16
16
|
import { UsagePeriod } from '@schematichq/schematic-js';
|
|
17
17
|
|
|
18
|
-
declare type BaseSchematicProviderProps = Omit<SchematicJS.SchematicOptions, "client" | "publishableKey"> & {
|
|
18
|
+
declare type BaseSchematicProviderProps = Omit<SchematicJS.SchematicOptions, "client" | "publishableKey" | "useWebSocket"> & {
|
|
19
19
|
children: React_2.ReactNode;
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -754,7 +754,7 @@ function contextString(context) {
|
|
|
754
754
|
}, {});
|
|
755
755
|
return JSON.stringify(sortedContext);
|
|
756
756
|
}
|
|
757
|
-
var version = "1.2.
|
|
757
|
+
var version = "1.2.12";
|
|
758
758
|
var anonymousIdKey = "schematicId";
|
|
759
759
|
var Schematic = class {
|
|
760
760
|
additionalHeaders = {};
|
|
@@ -1565,6 +1565,130 @@ var Schematic = class {
|
|
|
1565
1565
|
/**
|
|
1566
1566
|
* Websocket management
|
|
1567
1567
|
*/
|
|
1568
|
+
/**
|
|
1569
|
+
* Force an immediate WebSocket reconnection.
|
|
1570
|
+
* This is useful when the application returns from a background state (e.g., mobile app
|
|
1571
|
+
* coming back to foreground) and wants to immediately re-establish the connection
|
|
1572
|
+
* rather than waiting for the exponential backoff timer.
|
|
1573
|
+
*
|
|
1574
|
+
* This method will:
|
|
1575
|
+
* - Cancel any pending reconnection timer
|
|
1576
|
+
* - Reset the reconnection attempt counter
|
|
1577
|
+
* - Close any existing connection
|
|
1578
|
+
* - Immediately attempt to reconnect
|
|
1579
|
+
* - Re-send the current context to get fresh flag values
|
|
1580
|
+
*
|
|
1581
|
+
* Use this when you need guaranteed fresh values (e.g., after an in-app purchase).
|
|
1582
|
+
*
|
|
1583
|
+
* @example
|
|
1584
|
+
* ```typescript
|
|
1585
|
+
* // React Native example: reconnect when app comes to foreground
|
|
1586
|
+
* useEffect(() => {
|
|
1587
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1588
|
+
* if (state === "active") {
|
|
1589
|
+
* client.forceReconnect();
|
|
1590
|
+
* }
|
|
1591
|
+
* });
|
|
1592
|
+
* return () => subscription.remove();
|
|
1593
|
+
* }, [client]);
|
|
1594
|
+
* ```
|
|
1595
|
+
*/
|
|
1596
|
+
forceReconnect = async () => {
|
|
1597
|
+
return this.reconnect({ force: true });
|
|
1598
|
+
};
|
|
1599
|
+
/**
|
|
1600
|
+
* Reconnect the WebSocket connection only if the current connection is unhealthy.
|
|
1601
|
+
* This is useful when the application returns from a background state and wants to
|
|
1602
|
+
* ensure a healthy connection exists, but doesn't need to force a reconnection if
|
|
1603
|
+
* the connection is still active.
|
|
1604
|
+
*
|
|
1605
|
+
* This method will:
|
|
1606
|
+
* - Check if an existing connection is healthy (readyState === OPEN)
|
|
1607
|
+
* - If healthy, return immediately without reconnecting
|
|
1608
|
+
* - If unhealthy, perform the same reconnection logic as forceReconnect()
|
|
1609
|
+
*
|
|
1610
|
+
* Use this when you want efficient reconnection that avoids unnecessary disconnects.
|
|
1611
|
+
*
|
|
1612
|
+
* @example
|
|
1613
|
+
* ```typescript
|
|
1614
|
+
* // React Native example: reconnect only if needed when app comes to foreground
|
|
1615
|
+
* useEffect(() => {
|
|
1616
|
+
* const subscription = AppState.addEventListener("change", (state) => {
|
|
1617
|
+
* if (state === "active") {
|
|
1618
|
+
* client.reconnectIfNeeded();
|
|
1619
|
+
* }
|
|
1620
|
+
* });
|
|
1621
|
+
* return () => subscription.remove();
|
|
1622
|
+
* }, [client]);
|
|
1623
|
+
* ```
|
|
1624
|
+
*/
|
|
1625
|
+
reconnectIfNeeded = async () => {
|
|
1626
|
+
return this.reconnect({ force: false });
|
|
1627
|
+
};
|
|
1628
|
+
/**
|
|
1629
|
+
* Internal method to handle reconnection logic for both forceReconnect and reconnectIfNeeded.
|
|
1630
|
+
*/
|
|
1631
|
+
reconnect = async (options) => {
|
|
1632
|
+
const { force } = options;
|
|
1633
|
+
const methodName = force ? "forceReconnect" : "reconnectIfNeeded";
|
|
1634
|
+
if (this.isOffline()) {
|
|
1635
|
+
this.debug(`${methodName}: skipped (offline mode)`);
|
|
1636
|
+
return Promise.resolve();
|
|
1637
|
+
}
|
|
1638
|
+
if (!force && this.conn !== null) {
|
|
1639
|
+
try {
|
|
1640
|
+
const existingSocket = await this.conn;
|
|
1641
|
+
if (existingSocket.readyState === WebSocket.OPEN) {
|
|
1642
|
+
this.debug(`${methodName}: connection is healthy, skipping`);
|
|
1643
|
+
return Promise.resolve();
|
|
1644
|
+
}
|
|
1645
|
+
} catch {
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
this.debug(
|
|
1649
|
+
`${methodName}: ${force ? "forcing immediate reconnection" : "reconnecting"}`
|
|
1650
|
+
);
|
|
1651
|
+
this.wsIntentionalDisconnect = false;
|
|
1652
|
+
if (this.wsReconnectTimer !== null) {
|
|
1653
|
+
this.debug(`${methodName}: cancelling pending reconnection timer`);
|
|
1654
|
+
clearTimeout(this.wsReconnectTimer);
|
|
1655
|
+
this.wsReconnectTimer = null;
|
|
1656
|
+
}
|
|
1657
|
+
this.wsReconnectAttempts = 0;
|
|
1658
|
+
if (this.conn !== null) {
|
|
1659
|
+
this.debug(`${methodName}: closing existing connection`);
|
|
1660
|
+
try {
|
|
1661
|
+
const existingSocket = await this.conn;
|
|
1662
|
+
if (this.currentWebSocket === existingSocket) {
|
|
1663
|
+
this.currentWebSocket = null;
|
|
1664
|
+
}
|
|
1665
|
+
if (existingSocket.readyState === WebSocket.OPEN || existingSocket.readyState === WebSocket.CONNECTING) {
|
|
1666
|
+
existingSocket.close();
|
|
1667
|
+
}
|
|
1668
|
+
} catch (error) {
|
|
1669
|
+
this.debug(`${methodName}: error closing existing connection:`, error);
|
|
1670
|
+
}
|
|
1671
|
+
this.conn = null;
|
|
1672
|
+
this.isConnecting = false;
|
|
1673
|
+
}
|
|
1674
|
+
if (this.context.company !== void 0 || this.context.user !== void 0) {
|
|
1675
|
+
this.debug(`${methodName}: reconnecting with existing context`);
|
|
1676
|
+
try {
|
|
1677
|
+
this.isConnecting = true;
|
|
1678
|
+
this.conn = this.wsConnect();
|
|
1679
|
+
const socket = await this.conn;
|
|
1680
|
+
this.isConnecting = false;
|
|
1681
|
+
await this.wsSendMessage(socket, this.context, true);
|
|
1682
|
+
this.debug(`${methodName}: reconnection successful`);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
this.isConnecting = false;
|
|
1685
|
+
this.debug(`${methodName}: reconnection failed:`, error);
|
|
1686
|
+
}
|
|
1687
|
+
} else {
|
|
1688
|
+
this.debug(`${methodName}: no context set, skipping reconnection`);
|
|
1689
|
+
}
|
|
1690
|
+
return Promise.resolve();
|
|
1691
|
+
};
|
|
1568
1692
|
/**
|
|
1569
1693
|
* If using websocket mode, close the connection when done.
|
|
1570
1694
|
* In offline mode, this is a no-op.
|
|
@@ -1968,7 +2092,7 @@ var notifyFlagValueListener = (listener, value) => {
|
|
|
1968
2092
|
import React, { createContext, useEffect, useMemo, useRef } from "react";
|
|
1969
2093
|
|
|
1970
2094
|
// src/version.ts
|
|
1971
|
-
var version2 = "1.2.
|
|
2095
|
+
var version2 = "1.2.12";
|
|
1972
2096
|
|
|
1973
2097
|
// src/context/schematic.tsx
|
|
1974
2098
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -1983,7 +2107,7 @@ var SchematicProvider = ({
|
|
|
1983
2107
|
}) => {
|
|
1984
2108
|
const initialOptsRef = useRef({
|
|
1985
2109
|
publishableKey,
|
|
1986
|
-
useWebSocket:
|
|
2110
|
+
useWebSocket: true,
|
|
1987
2111
|
additionalHeaders: {
|
|
1988
2112
|
"X-Schematic-Client-Version": `schematic-react@${version2}`
|
|
1989
2113
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schematichq/schematic-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.12",
|
|
4
4
|
"main": "dist/schematic-react.cjs.js",
|
|
5
5
|
"module": "dist/schematic-react.esm.js",
|
|
6
6
|
"types": "dist/schematic-react.d.ts",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"prepare": "husky"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@schematichq/schematic-js": "^1.2.
|
|
34
|
+
"@schematichq/schematic-js": "^1.2.12"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^9.39.1",
|