@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 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.10";
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.10";
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: clientOpts.useWebSocket ?? true,
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.10";
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.10";
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: clientOpts.useWebSocket ?? true,
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.10",
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.10"
34
+ "@schematichq/schematic-js": "^1.2.12"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^9.39.1",