@blinkdotnew/sdk 0.19.6 → 2.0.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/dist/index.d.mts +41 -42
- package/dist/index.d.ts +41 -42
- package/dist/index.js +128 -75
- package/dist/index.mjs +128 -75
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -1054,18 +1054,37 @@ declare class BlinkAuth {
|
|
|
1054
1054
|
signInWithEmail(email: string, password: string): Promise<BlinkUser>;
|
|
1055
1055
|
/**
|
|
1056
1056
|
* Sign in with Google (headless mode)
|
|
1057
|
+
*
|
|
1058
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1059
|
+
*
|
|
1060
|
+
* On React Native, requires `webBrowser` to be configured in client:
|
|
1061
|
+
* ```typescript
|
|
1062
|
+
* const blink = createClient({
|
|
1063
|
+
* auth: { mode: 'headless', webBrowser: WebBrowser }
|
|
1064
|
+
* })
|
|
1065
|
+
* await blink.auth.signInWithGoogle() // Works on both platforms!
|
|
1066
|
+
* ```
|
|
1057
1067
|
*/
|
|
1058
1068
|
signInWithGoogle(options?: AuthOptions): Promise<BlinkUser>;
|
|
1059
1069
|
/**
|
|
1060
1070
|
* Sign in with GitHub (headless mode)
|
|
1071
|
+
*
|
|
1072
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1073
|
+
* See signInWithGoogle() for setup instructions.
|
|
1061
1074
|
*/
|
|
1062
1075
|
signInWithGitHub(options?: AuthOptions): Promise<BlinkUser>;
|
|
1063
1076
|
/**
|
|
1064
1077
|
* Sign in with Apple (headless mode)
|
|
1078
|
+
*
|
|
1079
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1080
|
+
* See signInWithGoogle() for setup instructions.
|
|
1065
1081
|
*/
|
|
1066
1082
|
signInWithApple(options?: AuthOptions): Promise<BlinkUser>;
|
|
1067
1083
|
/**
|
|
1068
1084
|
* Sign in with Microsoft (headless mode)
|
|
1085
|
+
*
|
|
1086
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1087
|
+
* See signInWithGoogle() for setup instructions.
|
|
1069
1088
|
*/
|
|
1070
1089
|
signInWithMicrosoft(options?: AuthOptions): Promise<BlinkUser>;
|
|
1071
1090
|
/**
|
|
@@ -1151,56 +1170,36 @@ declare class BlinkAuth {
|
|
|
1151
1170
|
authenticate: () => Promise<BlinkUser>;
|
|
1152
1171
|
}>;
|
|
1153
1172
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
authUrl: string;
|
|
1158
|
-
authenticate: () => Promise<BlinkUser>;
|
|
1159
|
-
}>;
|
|
1160
|
-
/**
|
|
1161
|
-
* Sign in with GitHub using expo-web-browser (React Native convenience)
|
|
1173
|
+
* Universal OAuth flow using session-based authentication (internal)
|
|
1174
|
+
* Works on ALL platforms: Web, iOS, Android
|
|
1175
|
+
* Uses expo-web-browser to open auth URL and polls for completion
|
|
1162
1176
|
*/
|
|
1163
|
-
|
|
1164
|
-
authUrl: string;
|
|
1165
|
-
authenticate: () => Promise<BlinkUser>;
|
|
1166
|
-
}>;
|
|
1167
|
-
/**
|
|
1168
|
-
* Sign in with Apple using expo-web-browser (React Native convenience)
|
|
1169
|
-
*/
|
|
1170
|
-
signInWithAppleMobile(options?: Omit<AuthOptions, 'redirectUrl'>): Promise<{
|
|
1171
|
-
authUrl: string;
|
|
1172
|
-
authenticate: () => Promise<BlinkUser>;
|
|
1173
|
-
}>;
|
|
1174
|
-
/**
|
|
1175
|
-
* React Native OAuth flow using expo-web-browser (internal)
|
|
1176
|
-
* Automatically handles opening browser and extracting tokens from redirect
|
|
1177
|
-
*/
|
|
1178
|
-
private signInWithProviderReactNative;
|
|
1177
|
+
private signInWithProviderUniversal;
|
|
1179
1178
|
/**
|
|
1180
1179
|
* Generic provider sign-in method (headless mode)
|
|
1181
1180
|
*
|
|
1182
|
-
*
|
|
1181
|
+
* **Universal OAuth** - Works seamlessly on both Web and React Native!
|
|
1183
1182
|
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
* ```json
|
|
1187
|
-
* { "expo": { "scheme": "com.yourapp" } }
|
|
1188
|
-
* ```
|
|
1183
|
+
* When `webBrowser` is configured in the client, this method automatically
|
|
1184
|
+
* uses the session-based OAuth flow that works on ALL platforms.
|
|
1189
1185
|
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1186
|
+
* **Universal Setup (configure once, works everywhere):**
|
|
1187
|
+
* ```typescript
|
|
1188
|
+
* import * as WebBrowser from 'expo-web-browser'
|
|
1189
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
1192
1190
|
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1191
|
+
* const blink = createClient({
|
|
1192
|
+
* projectId: 'your-project',
|
|
1193
|
+
* auth: {
|
|
1194
|
+
* mode: 'headless',
|
|
1195
|
+
* webBrowser: WebBrowser // Pass the module here
|
|
1196
|
+
* },
|
|
1197
|
+
* storage: new AsyncStorageAdapter(AsyncStorage)
|
|
1198
|
+
* })
|
|
1196
1199
|
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
* await blink.auth.setSession(queryParams)
|
|
1201
|
-
* }
|
|
1202
|
-
* })
|
|
1203
|
-
* ```
|
|
1200
|
+
* // Now this works on ALL platforms - no platform checks needed!
|
|
1201
|
+
* const user = await blink.auth.signInWithGoogle()
|
|
1202
|
+
* ```
|
|
1204
1203
|
*
|
|
1205
1204
|
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1206
1205
|
* @param options - Optional redirect URL and metadata
|
package/dist/index.d.ts
CHANGED
|
@@ -1054,18 +1054,37 @@ declare class BlinkAuth {
|
|
|
1054
1054
|
signInWithEmail(email: string, password: string): Promise<BlinkUser>;
|
|
1055
1055
|
/**
|
|
1056
1056
|
* Sign in with Google (headless mode)
|
|
1057
|
+
*
|
|
1058
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1059
|
+
*
|
|
1060
|
+
* On React Native, requires `webBrowser` to be configured in client:
|
|
1061
|
+
* ```typescript
|
|
1062
|
+
* const blink = createClient({
|
|
1063
|
+
* auth: { mode: 'headless', webBrowser: WebBrowser }
|
|
1064
|
+
* })
|
|
1065
|
+
* await blink.auth.signInWithGoogle() // Works on both platforms!
|
|
1066
|
+
* ```
|
|
1057
1067
|
*/
|
|
1058
1068
|
signInWithGoogle(options?: AuthOptions): Promise<BlinkUser>;
|
|
1059
1069
|
/**
|
|
1060
1070
|
* Sign in with GitHub (headless mode)
|
|
1071
|
+
*
|
|
1072
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1073
|
+
* See signInWithGoogle() for setup instructions.
|
|
1061
1074
|
*/
|
|
1062
1075
|
signInWithGitHub(options?: AuthOptions): Promise<BlinkUser>;
|
|
1063
1076
|
/**
|
|
1064
1077
|
* Sign in with Apple (headless mode)
|
|
1078
|
+
*
|
|
1079
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1080
|
+
* See signInWithGoogle() for setup instructions.
|
|
1065
1081
|
*/
|
|
1066
1082
|
signInWithApple(options?: AuthOptions): Promise<BlinkUser>;
|
|
1067
1083
|
/**
|
|
1068
1084
|
* Sign in with Microsoft (headless mode)
|
|
1085
|
+
*
|
|
1086
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1087
|
+
* See signInWithGoogle() for setup instructions.
|
|
1069
1088
|
*/
|
|
1070
1089
|
signInWithMicrosoft(options?: AuthOptions): Promise<BlinkUser>;
|
|
1071
1090
|
/**
|
|
@@ -1151,56 +1170,36 @@ declare class BlinkAuth {
|
|
|
1151
1170
|
authenticate: () => Promise<BlinkUser>;
|
|
1152
1171
|
}>;
|
|
1153
1172
|
/**
|
|
1154
|
-
*
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
authUrl: string;
|
|
1158
|
-
authenticate: () => Promise<BlinkUser>;
|
|
1159
|
-
}>;
|
|
1160
|
-
/**
|
|
1161
|
-
* Sign in with GitHub using expo-web-browser (React Native convenience)
|
|
1173
|
+
* Universal OAuth flow using session-based authentication (internal)
|
|
1174
|
+
* Works on ALL platforms: Web, iOS, Android
|
|
1175
|
+
* Uses expo-web-browser to open auth URL and polls for completion
|
|
1162
1176
|
*/
|
|
1163
|
-
|
|
1164
|
-
authUrl: string;
|
|
1165
|
-
authenticate: () => Promise<BlinkUser>;
|
|
1166
|
-
}>;
|
|
1167
|
-
/**
|
|
1168
|
-
* Sign in with Apple using expo-web-browser (React Native convenience)
|
|
1169
|
-
*/
|
|
1170
|
-
signInWithAppleMobile(options?: Omit<AuthOptions, 'redirectUrl'>): Promise<{
|
|
1171
|
-
authUrl: string;
|
|
1172
|
-
authenticate: () => Promise<BlinkUser>;
|
|
1173
|
-
}>;
|
|
1174
|
-
/**
|
|
1175
|
-
* React Native OAuth flow using expo-web-browser (internal)
|
|
1176
|
-
* Automatically handles opening browser and extracting tokens from redirect
|
|
1177
|
-
*/
|
|
1178
|
-
private signInWithProviderReactNative;
|
|
1177
|
+
private signInWithProviderUniversal;
|
|
1179
1178
|
/**
|
|
1180
1179
|
* Generic provider sign-in method (headless mode)
|
|
1181
1180
|
*
|
|
1182
|
-
*
|
|
1181
|
+
* **Universal OAuth** - Works seamlessly on both Web and React Native!
|
|
1183
1182
|
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
1186
|
-
* ```json
|
|
1187
|
-
* { "expo": { "scheme": "com.yourapp" } }
|
|
1188
|
-
* ```
|
|
1183
|
+
* When `webBrowser` is configured in the client, this method automatically
|
|
1184
|
+
* uses the session-based OAuth flow that works on ALL platforms.
|
|
1189
1185
|
*
|
|
1190
|
-
*
|
|
1191
|
-
*
|
|
1186
|
+
* **Universal Setup (configure once, works everywhere):**
|
|
1187
|
+
* ```typescript
|
|
1188
|
+
* import * as WebBrowser from 'expo-web-browser'
|
|
1189
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
1192
1190
|
*
|
|
1193
|
-
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1191
|
+
* const blink = createClient({
|
|
1192
|
+
* projectId: 'your-project',
|
|
1193
|
+
* auth: {
|
|
1194
|
+
* mode: 'headless',
|
|
1195
|
+
* webBrowser: WebBrowser // Pass the module here
|
|
1196
|
+
* },
|
|
1197
|
+
* storage: new AsyncStorageAdapter(AsyncStorage)
|
|
1198
|
+
* })
|
|
1196
1199
|
*
|
|
1197
|
-
*
|
|
1198
|
-
*
|
|
1199
|
-
*
|
|
1200
|
-
* await blink.auth.setSession(queryParams)
|
|
1201
|
-
* }
|
|
1202
|
-
* })
|
|
1203
|
-
* ```
|
|
1200
|
+
* // Now this works on ALL platforms - no platform checks needed!
|
|
1201
|
+
* const user = await blink.auth.signInWithGoogle()
|
|
1202
|
+
* ```
|
|
1204
1203
|
*
|
|
1205
1204
|
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1206
1205
|
* @param options - Optional redirect URL and metadata
|
package/dist/index.js
CHANGED
|
@@ -1238,10 +1238,7 @@ var BlinkAuth = class {
|
|
|
1238
1238
|
setupParentWindowListener() {
|
|
1239
1239
|
if (!isWeb || !this.isIframe || !hasWindow()) return;
|
|
1240
1240
|
window.addEventListener("message", (event) => {
|
|
1241
|
-
|
|
1242
|
-
const isTrustedOrigin = origin === "https://blink.new" || origin === "http://localhost:3000" || origin === "http://localhost:3001" || origin.endsWith(".sites.blink.new") || // Trust all preview URLs
|
|
1243
|
-
origin.endsWith(".preview-blink.com");
|
|
1244
|
-
if (!isTrustedOrigin) {
|
|
1241
|
+
if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
|
|
1245
1242
|
return;
|
|
1246
1243
|
}
|
|
1247
1244
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
@@ -1617,6 +1614,16 @@ var BlinkAuth = class {
|
|
|
1617
1614
|
}
|
|
1618
1615
|
/**
|
|
1619
1616
|
* Sign in with Google (headless mode)
|
|
1617
|
+
*
|
|
1618
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1619
|
+
*
|
|
1620
|
+
* On React Native, requires `webBrowser` to be configured in client:
|
|
1621
|
+
* ```typescript
|
|
1622
|
+
* const blink = createClient({
|
|
1623
|
+
* auth: { mode: 'headless', webBrowser: WebBrowser }
|
|
1624
|
+
* })
|
|
1625
|
+
* await blink.auth.signInWithGoogle() // Works on both platforms!
|
|
1626
|
+
* ```
|
|
1620
1627
|
*/
|
|
1621
1628
|
async signInWithGoogle(options) {
|
|
1622
1629
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1626,6 +1633,9 @@ var BlinkAuth = class {
|
|
|
1626
1633
|
}
|
|
1627
1634
|
/**
|
|
1628
1635
|
* Sign in with GitHub (headless mode)
|
|
1636
|
+
*
|
|
1637
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1638
|
+
* See signInWithGoogle() for setup instructions.
|
|
1629
1639
|
*/
|
|
1630
1640
|
async signInWithGitHub(options) {
|
|
1631
1641
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1635,6 +1645,9 @@ var BlinkAuth = class {
|
|
|
1635
1645
|
}
|
|
1636
1646
|
/**
|
|
1637
1647
|
* Sign in with Apple (headless mode)
|
|
1648
|
+
*
|
|
1649
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1650
|
+
* See signInWithGoogle() for setup instructions.
|
|
1638
1651
|
*/
|
|
1639
1652
|
async signInWithApple(options) {
|
|
1640
1653
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1644,6 +1657,9 @@ var BlinkAuth = class {
|
|
|
1644
1657
|
}
|
|
1645
1658
|
/**
|
|
1646
1659
|
* Sign in with Microsoft (headless mode)
|
|
1660
|
+
*
|
|
1661
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1662
|
+
* See signInWithGoogle() for setup instructions.
|
|
1647
1663
|
*/
|
|
1648
1664
|
async signInWithMicrosoft(options) {
|
|
1649
1665
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1805,58 +1821,65 @@ var BlinkAuth = class {
|
|
|
1805
1821
|
};
|
|
1806
1822
|
}
|
|
1807
1823
|
/**
|
|
1808
|
-
*
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
return this.signInWithProviderMobile("google", options);
|
|
1812
|
-
}
|
|
1813
|
-
/**
|
|
1814
|
-
* Sign in with GitHub using expo-web-browser (React Native convenience)
|
|
1815
|
-
*/
|
|
1816
|
-
async signInWithGitHubMobile(options) {
|
|
1817
|
-
return this.signInWithProviderMobile("github", options);
|
|
1818
|
-
}
|
|
1819
|
-
/**
|
|
1820
|
-
* Sign in with Apple using expo-web-browser (React Native convenience)
|
|
1821
|
-
*/
|
|
1822
|
-
async signInWithAppleMobile(options) {
|
|
1823
|
-
return this.signInWithProviderMobile("apple", options);
|
|
1824
|
-
}
|
|
1825
|
-
/**
|
|
1826
|
-
* React Native OAuth flow using expo-web-browser (internal)
|
|
1827
|
-
* Automatically handles opening browser and extracting tokens from redirect
|
|
1824
|
+
* Universal OAuth flow using session-based authentication (internal)
|
|
1825
|
+
* Works on ALL platforms: Web, iOS, Android
|
|
1826
|
+
* Uses expo-web-browser to open auth URL and polls for completion
|
|
1828
1827
|
*/
|
|
1829
|
-
async
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1828
|
+
async signInWithProviderUniversal(provider, options) {
|
|
1829
|
+
const webBrowser = this.authConfig.webBrowser;
|
|
1830
|
+
if (!webBrowser) {
|
|
1831
|
+
throw new BlinkAuthError(
|
|
1832
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1833
|
+
"webBrowser module is required for universal OAuth flow"
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
|
|
1837
|
+
console.log("\u{1F510} Opening OAuth browser for", provider);
|
|
1838
|
+
const result = await webBrowser.openAuthSessionAsync(authUrl);
|
|
1839
|
+
console.log("\u{1F510} Browser closed with result:", result.type);
|
|
1840
|
+
try {
|
|
1841
|
+
const user = await this.pollMobileOAuthSession(sessionId, {
|
|
1842
|
+
maxAttempts: 60,
|
|
1843
|
+
// 30 seconds (500ms intervals)
|
|
1844
|
+
intervalMs: 500
|
|
1845
|
+
});
|
|
1846
|
+
console.log("\u2705 OAuth completed successfully");
|
|
1847
|
+
return user;
|
|
1848
|
+
} catch (pollError) {
|
|
1849
|
+
if (result.type === "cancel" || result.type === "dismiss") {
|
|
1850
|
+
throw new BlinkAuthError(
|
|
1851
|
+
"POPUP_CANCELED" /* POPUP_CANCELED */,
|
|
1852
|
+
"Authentication was canceled"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
throw pollError;
|
|
1856
|
+
}
|
|
1834
1857
|
}
|
|
1835
1858
|
/**
|
|
1836
1859
|
* Generic provider sign-in method (headless mode)
|
|
1837
1860
|
*
|
|
1838
|
-
*
|
|
1861
|
+
* **Universal OAuth** - Works seamlessly on both Web and React Native!
|
|
1862
|
+
*
|
|
1863
|
+
* When `webBrowser` is configured in the client, this method automatically
|
|
1864
|
+
* uses the session-based OAuth flow that works on ALL platforms.
|
|
1839
1865
|
*
|
|
1840
|
-
* **
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
1843
|
-
*
|
|
1844
|
-
* ```
|
|
1866
|
+
* **Universal Setup (configure once, works everywhere):**
|
|
1867
|
+
* ```typescript
|
|
1868
|
+
* import * as WebBrowser from 'expo-web-browser'
|
|
1869
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
1845
1870
|
*
|
|
1846
|
-
*
|
|
1847
|
-
*
|
|
1871
|
+
* const blink = createClient({
|
|
1872
|
+
* projectId: 'your-project',
|
|
1873
|
+
* auth: {
|
|
1874
|
+
* mode: 'headless',
|
|
1875
|
+
* webBrowser: WebBrowser // Pass the module here
|
|
1876
|
+
* },
|
|
1877
|
+
* storage: new AsyncStorageAdapter(AsyncStorage)
|
|
1878
|
+
* })
|
|
1848
1879
|
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
1853
|
-
* Linking.addEventListener('url', async ({ url }) => {
|
|
1854
|
-
* const { queryParams } = Linking.parse(url)
|
|
1855
|
-
* if (queryParams.access_token) {
|
|
1856
|
-
* await blink.auth.setSession(queryParams)
|
|
1857
|
-
* }
|
|
1858
|
-
* })
|
|
1859
|
-
* ```
|
|
1880
|
+
* // Now this works on ALL platforms - no platform checks needed!
|
|
1881
|
+
* const user = await blink.auth.signInWithGoogle()
|
|
1882
|
+
* ```
|
|
1860
1883
|
*
|
|
1861
1884
|
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1862
1885
|
* @param options - Optional redirect URL and metadata
|
|
@@ -1866,28 +1889,45 @@ var BlinkAuth = class {
|
|
|
1866
1889
|
if (this.authConfig.mode !== "headless") {
|
|
1867
1890
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1868
1891
|
}
|
|
1892
|
+
if (this.authConfig.webBrowser) {
|
|
1893
|
+
return this.signInWithProviderUniversal(provider, options);
|
|
1894
|
+
}
|
|
1895
|
+
if (isReactNative2()) {
|
|
1896
|
+
throw new BlinkAuthError(
|
|
1897
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1898
|
+
'React Native OAuth requires webBrowser in config!\n\nimport * as WebBrowser from "expo-web-browser";\n\nconst blink = createClient({\n projectId: "your-project",\n auth: {\n mode: "headless",\n webBrowser: WebBrowser\n }\n})\n\nawait blink.auth.signInWithGoogle() // Works on all platforms!'
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1869
1901
|
if (!hasWindow()) {
|
|
1870
1902
|
throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
|
|
1871
1903
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
1904
|
+
const shouldPreferRedirect = isWeb && this.isIframe || typeof window !== "undefined" && window.crossOriginIsolated === true;
|
|
1905
|
+
const state = this.generateState();
|
|
1906
|
+
try {
|
|
1907
|
+
const sessionStorage = getSessionStorage();
|
|
1908
|
+
if (sessionStorage) {
|
|
1909
|
+
sessionStorage.setItem("blink_oauth_state", state);
|
|
1910
|
+
}
|
|
1911
|
+
} catch {
|
|
1912
|
+
}
|
|
1913
|
+
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1914
|
+
const buildAuthUrl = (mode) => {
|
|
1915
|
+
const url = new URL("/auth", this.authUrl);
|
|
1916
|
+
url.searchParams.set("provider", provider);
|
|
1917
|
+
url.searchParams.set("project_id", this.config.projectId);
|
|
1918
|
+
url.searchParams.set("state", state);
|
|
1919
|
+
url.searchParams.set("mode", mode);
|
|
1920
|
+
url.searchParams.set("redirect_url", redirectUrl);
|
|
1921
|
+
url.searchParams.set("opener_origin", getLocationOrigin() || "");
|
|
1922
|
+
return url;
|
|
1923
|
+
};
|
|
1924
|
+
if (shouldPreferRedirect) {
|
|
1925
|
+
window.location.href = buildAuthUrl("redirect").toString();
|
|
1926
|
+
return new Promise(() => {
|
|
1927
|
+
});
|
|
1874
1928
|
}
|
|
1875
1929
|
return new Promise((resolve, reject) => {
|
|
1876
|
-
const
|
|
1877
|
-
try {
|
|
1878
|
-
const sessionStorage = getSessionStorage();
|
|
1879
|
-
if (sessionStorage) {
|
|
1880
|
-
sessionStorage.setItem("blink_oauth_state", state);
|
|
1881
|
-
}
|
|
1882
|
-
} catch {
|
|
1883
|
-
}
|
|
1884
|
-
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1885
|
-
const popupUrl = new URL("/auth", this.authUrl);
|
|
1886
|
-
popupUrl.searchParams.set("provider", provider);
|
|
1887
|
-
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
1888
|
-
popupUrl.searchParams.set("state", state);
|
|
1889
|
-
popupUrl.searchParams.set("mode", "popup");
|
|
1890
|
-
popupUrl.searchParams.set("redirect_url", redirectUrl);
|
|
1930
|
+
const popupUrl = buildAuthUrl("popup");
|
|
1891
1931
|
const popup = window.open(
|
|
1892
1932
|
popupUrl.toString(),
|
|
1893
1933
|
"blink-auth",
|
|
@@ -1898,6 +1938,15 @@ var BlinkAuth = class {
|
|
|
1898
1938
|
return;
|
|
1899
1939
|
}
|
|
1900
1940
|
let timeoutId;
|
|
1941
|
+
let closedIntervalId;
|
|
1942
|
+
let cleanedUp = false;
|
|
1943
|
+
const cleanup = () => {
|
|
1944
|
+
if (cleanedUp) return;
|
|
1945
|
+
cleanedUp = true;
|
|
1946
|
+
clearTimeout(timeoutId);
|
|
1947
|
+
if (closedIntervalId) clearInterval(closedIntervalId);
|
|
1948
|
+
window.removeEventListener("message", messageListener);
|
|
1949
|
+
};
|
|
1901
1950
|
const messageListener = (event) => {
|
|
1902
1951
|
let allowed = false;
|
|
1903
1952
|
try {
|
|
@@ -1906,7 +1955,6 @@ var BlinkAuth = class {
|
|
|
1906
1955
|
} catch {
|
|
1907
1956
|
}
|
|
1908
1957
|
if (event.origin === "http://localhost:3000" || event.origin === "http://localhost:3001") allowed = true;
|
|
1909
|
-
if (event.origin.endsWith(".sites.blink.new") || event.origin.endsWith(".preview-blink.com")) allowed = true;
|
|
1910
1958
|
if (!allowed) return;
|
|
1911
1959
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1912
1960
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
@@ -1935,29 +1983,34 @@ var BlinkAuth = class {
|
|
|
1935
1983
|
}, true).then(() => {
|
|
1936
1984
|
resolve(this.authState.user);
|
|
1937
1985
|
}).catch(reject);
|
|
1938
|
-
|
|
1939
|
-
window.removeEventListener("message", messageListener);
|
|
1986
|
+
cleanup();
|
|
1940
1987
|
popup.close();
|
|
1941
1988
|
} else if (event.data?.type === "BLINK_AUTH_ERROR") {
|
|
1942
1989
|
const errorCode = this.mapErrorCodeFromResponse(event.data.code);
|
|
1943
1990
|
reject(new BlinkAuthError(errorCode, event.data.message || "Authentication failed"));
|
|
1944
|
-
|
|
1945
|
-
window.removeEventListener("message", messageListener);
|
|
1991
|
+
cleanup();
|
|
1946
1992
|
popup.close();
|
|
1947
1993
|
}
|
|
1948
1994
|
};
|
|
1995
|
+
if (popup.opener === null) {
|
|
1996
|
+
try {
|
|
1997
|
+
popup.close();
|
|
1998
|
+
} catch {
|
|
1999
|
+
}
|
|
2000
|
+
cleanup();
|
|
2001
|
+
window.location.href = buildAuthUrl("redirect").toString();
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
1949
2004
|
timeoutId = setTimeout(() => {
|
|
1950
|
-
|
|
2005
|
+
cleanup();
|
|
1951
2006
|
if (!popup.closed) {
|
|
1952
2007
|
popup.close();
|
|
1953
2008
|
}
|
|
1954
2009
|
reject(new BlinkAuthError("AUTH_TIMEOUT" /* AUTH_TIMEOUT */, "Authentication timed out"));
|
|
1955
2010
|
}, 3e5);
|
|
1956
|
-
|
|
2011
|
+
closedIntervalId = setInterval(() => {
|
|
1957
2012
|
if (popup.closed) {
|
|
1958
|
-
|
|
1959
|
-
clearTimeout(timeoutId);
|
|
1960
|
-
window.removeEventListener("message", messageListener);
|
|
2013
|
+
cleanup();
|
|
1961
2014
|
reject(new BlinkAuthError("POPUP_CANCELED" /* POPUP_CANCELED */, "Authentication was canceled"));
|
|
1962
2015
|
}
|
|
1963
2016
|
}, 1e3);
|
package/dist/index.mjs
CHANGED
|
@@ -1236,10 +1236,7 @@ var BlinkAuth = class {
|
|
|
1236
1236
|
setupParentWindowListener() {
|
|
1237
1237
|
if (!isWeb || !this.isIframe || !hasWindow()) return;
|
|
1238
1238
|
window.addEventListener("message", (event) => {
|
|
1239
|
-
|
|
1240
|
-
const isTrustedOrigin = origin === "https://blink.new" || origin === "http://localhost:3000" || origin === "http://localhost:3001" || origin.endsWith(".sites.blink.new") || // Trust all preview URLs
|
|
1241
|
-
origin.endsWith(".preview-blink.com");
|
|
1242
|
-
if (!isTrustedOrigin) {
|
|
1239
|
+
if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
|
|
1243
1240
|
return;
|
|
1244
1241
|
}
|
|
1245
1242
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
@@ -1615,6 +1612,16 @@ var BlinkAuth = class {
|
|
|
1615
1612
|
}
|
|
1616
1613
|
/**
|
|
1617
1614
|
* Sign in with Google (headless mode)
|
|
1615
|
+
*
|
|
1616
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1617
|
+
*
|
|
1618
|
+
* On React Native, requires `webBrowser` to be configured in client:
|
|
1619
|
+
* ```typescript
|
|
1620
|
+
* const blink = createClient({
|
|
1621
|
+
* auth: { mode: 'headless', webBrowser: WebBrowser }
|
|
1622
|
+
* })
|
|
1623
|
+
* await blink.auth.signInWithGoogle() // Works on both platforms!
|
|
1624
|
+
* ```
|
|
1618
1625
|
*/
|
|
1619
1626
|
async signInWithGoogle(options) {
|
|
1620
1627
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1624,6 +1631,9 @@ var BlinkAuth = class {
|
|
|
1624
1631
|
}
|
|
1625
1632
|
/**
|
|
1626
1633
|
* Sign in with GitHub (headless mode)
|
|
1634
|
+
*
|
|
1635
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1636
|
+
* See signInWithGoogle() for setup instructions.
|
|
1627
1637
|
*/
|
|
1628
1638
|
async signInWithGitHub(options) {
|
|
1629
1639
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1633,6 +1643,9 @@ var BlinkAuth = class {
|
|
|
1633
1643
|
}
|
|
1634
1644
|
/**
|
|
1635
1645
|
* Sign in with Apple (headless mode)
|
|
1646
|
+
*
|
|
1647
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1648
|
+
* See signInWithGoogle() for setup instructions.
|
|
1636
1649
|
*/
|
|
1637
1650
|
async signInWithApple(options) {
|
|
1638
1651
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1642,6 +1655,9 @@ var BlinkAuth = class {
|
|
|
1642
1655
|
}
|
|
1643
1656
|
/**
|
|
1644
1657
|
* Sign in with Microsoft (headless mode)
|
|
1658
|
+
*
|
|
1659
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1660
|
+
* See signInWithGoogle() for setup instructions.
|
|
1645
1661
|
*/
|
|
1646
1662
|
async signInWithMicrosoft(options) {
|
|
1647
1663
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1803,58 +1819,65 @@ var BlinkAuth = class {
|
|
|
1803
1819
|
};
|
|
1804
1820
|
}
|
|
1805
1821
|
/**
|
|
1806
|
-
*
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
return this.signInWithProviderMobile("google", options);
|
|
1810
|
-
}
|
|
1811
|
-
/**
|
|
1812
|
-
* Sign in with GitHub using expo-web-browser (React Native convenience)
|
|
1813
|
-
*/
|
|
1814
|
-
async signInWithGitHubMobile(options) {
|
|
1815
|
-
return this.signInWithProviderMobile("github", options);
|
|
1816
|
-
}
|
|
1817
|
-
/**
|
|
1818
|
-
* Sign in with Apple using expo-web-browser (React Native convenience)
|
|
1819
|
-
*/
|
|
1820
|
-
async signInWithAppleMobile(options) {
|
|
1821
|
-
return this.signInWithProviderMobile("apple", options);
|
|
1822
|
-
}
|
|
1823
|
-
/**
|
|
1824
|
-
* React Native OAuth flow using expo-web-browser (internal)
|
|
1825
|
-
* Automatically handles opening browser and extracting tokens from redirect
|
|
1822
|
+
* Universal OAuth flow using session-based authentication (internal)
|
|
1823
|
+
* Works on ALL platforms: Web, iOS, Android
|
|
1824
|
+
* Uses expo-web-browser to open auth URL and polls for completion
|
|
1826
1825
|
*/
|
|
1827
|
-
async
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1826
|
+
async signInWithProviderUniversal(provider, options) {
|
|
1827
|
+
const webBrowser = this.authConfig.webBrowser;
|
|
1828
|
+
if (!webBrowser) {
|
|
1829
|
+
throw new BlinkAuthError(
|
|
1830
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1831
|
+
"webBrowser module is required for universal OAuth flow"
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
|
|
1835
|
+
console.log("\u{1F510} Opening OAuth browser for", provider);
|
|
1836
|
+
const result = await webBrowser.openAuthSessionAsync(authUrl);
|
|
1837
|
+
console.log("\u{1F510} Browser closed with result:", result.type);
|
|
1838
|
+
try {
|
|
1839
|
+
const user = await this.pollMobileOAuthSession(sessionId, {
|
|
1840
|
+
maxAttempts: 60,
|
|
1841
|
+
// 30 seconds (500ms intervals)
|
|
1842
|
+
intervalMs: 500
|
|
1843
|
+
});
|
|
1844
|
+
console.log("\u2705 OAuth completed successfully");
|
|
1845
|
+
return user;
|
|
1846
|
+
} catch (pollError) {
|
|
1847
|
+
if (result.type === "cancel" || result.type === "dismiss") {
|
|
1848
|
+
throw new BlinkAuthError(
|
|
1849
|
+
"POPUP_CANCELED" /* POPUP_CANCELED */,
|
|
1850
|
+
"Authentication was canceled"
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1853
|
+
throw pollError;
|
|
1854
|
+
}
|
|
1832
1855
|
}
|
|
1833
1856
|
/**
|
|
1834
1857
|
* Generic provider sign-in method (headless mode)
|
|
1835
1858
|
*
|
|
1836
|
-
*
|
|
1859
|
+
* **Universal OAuth** - Works seamlessly on both Web and React Native!
|
|
1860
|
+
*
|
|
1861
|
+
* When `webBrowser` is configured in the client, this method automatically
|
|
1862
|
+
* uses the session-based OAuth flow that works on ALL platforms.
|
|
1837
1863
|
*
|
|
1838
|
-
* **
|
|
1839
|
-
*
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
* ```
|
|
1864
|
+
* **Universal Setup (configure once, works everywhere):**
|
|
1865
|
+
* ```typescript
|
|
1866
|
+
* import * as WebBrowser from 'expo-web-browser'
|
|
1867
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
1843
1868
|
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1869
|
+
* const blink = createClient({
|
|
1870
|
+
* projectId: 'your-project',
|
|
1871
|
+
* auth: {
|
|
1872
|
+
* mode: 'headless',
|
|
1873
|
+
* webBrowser: WebBrowser // Pass the module here
|
|
1874
|
+
* },
|
|
1875
|
+
* storage: new AsyncStorageAdapter(AsyncStorage)
|
|
1876
|
+
* })
|
|
1846
1877
|
*
|
|
1847
|
-
*
|
|
1848
|
-
*
|
|
1849
|
-
*
|
|
1850
|
-
*
|
|
1851
|
-
* Linking.addEventListener('url', async ({ url }) => {
|
|
1852
|
-
* const { queryParams } = Linking.parse(url)
|
|
1853
|
-
* if (queryParams.access_token) {
|
|
1854
|
-
* await blink.auth.setSession(queryParams)
|
|
1855
|
-
* }
|
|
1856
|
-
* })
|
|
1857
|
-
* ```
|
|
1878
|
+
* // Now this works on ALL platforms - no platform checks needed!
|
|
1879
|
+
* const user = await blink.auth.signInWithGoogle()
|
|
1880
|
+
* ```
|
|
1858
1881
|
*
|
|
1859
1882
|
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1860
1883
|
* @param options - Optional redirect URL and metadata
|
|
@@ -1864,28 +1887,45 @@ var BlinkAuth = class {
|
|
|
1864
1887
|
if (this.authConfig.mode !== "headless") {
|
|
1865
1888
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1866
1889
|
}
|
|
1890
|
+
if (this.authConfig.webBrowser) {
|
|
1891
|
+
return this.signInWithProviderUniversal(provider, options);
|
|
1892
|
+
}
|
|
1893
|
+
if (isReactNative2()) {
|
|
1894
|
+
throw new BlinkAuthError(
|
|
1895
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1896
|
+
'React Native OAuth requires webBrowser in config!\n\nimport * as WebBrowser from "expo-web-browser";\n\nconst blink = createClient({\n projectId: "your-project",\n auth: {\n mode: "headless",\n webBrowser: WebBrowser\n }\n})\n\nawait blink.auth.signInWithGoogle() // Works on all platforms!'
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1867
1899
|
if (!hasWindow()) {
|
|
1868
1900
|
throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
|
|
1869
1901
|
}
|
|
1870
|
-
|
|
1871
|
-
|
|
1902
|
+
const shouldPreferRedirect = isWeb && this.isIframe || typeof window !== "undefined" && window.crossOriginIsolated === true;
|
|
1903
|
+
const state = this.generateState();
|
|
1904
|
+
try {
|
|
1905
|
+
const sessionStorage = getSessionStorage();
|
|
1906
|
+
if (sessionStorage) {
|
|
1907
|
+
sessionStorage.setItem("blink_oauth_state", state);
|
|
1908
|
+
}
|
|
1909
|
+
} catch {
|
|
1910
|
+
}
|
|
1911
|
+
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1912
|
+
const buildAuthUrl = (mode) => {
|
|
1913
|
+
const url = new URL("/auth", this.authUrl);
|
|
1914
|
+
url.searchParams.set("provider", provider);
|
|
1915
|
+
url.searchParams.set("project_id", this.config.projectId);
|
|
1916
|
+
url.searchParams.set("state", state);
|
|
1917
|
+
url.searchParams.set("mode", mode);
|
|
1918
|
+
url.searchParams.set("redirect_url", redirectUrl);
|
|
1919
|
+
url.searchParams.set("opener_origin", getLocationOrigin() || "");
|
|
1920
|
+
return url;
|
|
1921
|
+
};
|
|
1922
|
+
if (shouldPreferRedirect) {
|
|
1923
|
+
window.location.href = buildAuthUrl("redirect").toString();
|
|
1924
|
+
return new Promise(() => {
|
|
1925
|
+
});
|
|
1872
1926
|
}
|
|
1873
1927
|
return new Promise((resolve, reject) => {
|
|
1874
|
-
const
|
|
1875
|
-
try {
|
|
1876
|
-
const sessionStorage = getSessionStorage();
|
|
1877
|
-
if (sessionStorage) {
|
|
1878
|
-
sessionStorage.setItem("blink_oauth_state", state);
|
|
1879
|
-
}
|
|
1880
|
-
} catch {
|
|
1881
|
-
}
|
|
1882
|
-
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1883
|
-
const popupUrl = new URL("/auth", this.authUrl);
|
|
1884
|
-
popupUrl.searchParams.set("provider", provider);
|
|
1885
|
-
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
1886
|
-
popupUrl.searchParams.set("state", state);
|
|
1887
|
-
popupUrl.searchParams.set("mode", "popup");
|
|
1888
|
-
popupUrl.searchParams.set("redirect_url", redirectUrl);
|
|
1928
|
+
const popupUrl = buildAuthUrl("popup");
|
|
1889
1929
|
const popup = window.open(
|
|
1890
1930
|
popupUrl.toString(),
|
|
1891
1931
|
"blink-auth",
|
|
@@ -1896,6 +1936,15 @@ var BlinkAuth = class {
|
|
|
1896
1936
|
return;
|
|
1897
1937
|
}
|
|
1898
1938
|
let timeoutId;
|
|
1939
|
+
let closedIntervalId;
|
|
1940
|
+
let cleanedUp = false;
|
|
1941
|
+
const cleanup = () => {
|
|
1942
|
+
if (cleanedUp) return;
|
|
1943
|
+
cleanedUp = true;
|
|
1944
|
+
clearTimeout(timeoutId);
|
|
1945
|
+
if (closedIntervalId) clearInterval(closedIntervalId);
|
|
1946
|
+
window.removeEventListener("message", messageListener);
|
|
1947
|
+
};
|
|
1899
1948
|
const messageListener = (event) => {
|
|
1900
1949
|
let allowed = false;
|
|
1901
1950
|
try {
|
|
@@ -1904,7 +1953,6 @@ var BlinkAuth = class {
|
|
|
1904
1953
|
} catch {
|
|
1905
1954
|
}
|
|
1906
1955
|
if (event.origin === "http://localhost:3000" || event.origin === "http://localhost:3001") allowed = true;
|
|
1907
|
-
if (event.origin.endsWith(".sites.blink.new") || event.origin.endsWith(".preview-blink.com")) allowed = true;
|
|
1908
1956
|
if (!allowed) return;
|
|
1909
1957
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1910
1958
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
@@ -1933,29 +1981,34 @@ var BlinkAuth = class {
|
|
|
1933
1981
|
}, true).then(() => {
|
|
1934
1982
|
resolve(this.authState.user);
|
|
1935
1983
|
}).catch(reject);
|
|
1936
|
-
|
|
1937
|
-
window.removeEventListener("message", messageListener);
|
|
1984
|
+
cleanup();
|
|
1938
1985
|
popup.close();
|
|
1939
1986
|
} else if (event.data?.type === "BLINK_AUTH_ERROR") {
|
|
1940
1987
|
const errorCode = this.mapErrorCodeFromResponse(event.data.code);
|
|
1941
1988
|
reject(new BlinkAuthError(errorCode, event.data.message || "Authentication failed"));
|
|
1942
|
-
|
|
1943
|
-
window.removeEventListener("message", messageListener);
|
|
1989
|
+
cleanup();
|
|
1944
1990
|
popup.close();
|
|
1945
1991
|
}
|
|
1946
1992
|
};
|
|
1993
|
+
if (popup.opener === null) {
|
|
1994
|
+
try {
|
|
1995
|
+
popup.close();
|
|
1996
|
+
} catch {
|
|
1997
|
+
}
|
|
1998
|
+
cleanup();
|
|
1999
|
+
window.location.href = buildAuthUrl("redirect").toString();
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
1947
2002
|
timeoutId = setTimeout(() => {
|
|
1948
|
-
|
|
2003
|
+
cleanup();
|
|
1949
2004
|
if (!popup.closed) {
|
|
1950
2005
|
popup.close();
|
|
1951
2006
|
}
|
|
1952
2007
|
reject(new BlinkAuthError("AUTH_TIMEOUT" /* AUTH_TIMEOUT */, "Authentication timed out"));
|
|
1953
2008
|
}, 3e5);
|
|
1954
|
-
|
|
2009
|
+
closedIntervalId = setInterval(() => {
|
|
1955
2010
|
if (popup.closed) {
|
|
1956
|
-
|
|
1957
|
-
clearTimeout(timeoutId);
|
|
1958
|
-
window.removeEventListener("message", messageListener);
|
|
2011
|
+
cleanup();
|
|
1959
2012
|
reject(new BlinkAuthError("POPUP_CANCELED" /* POPUP_CANCELED */, "Authentication was canceled"));
|
|
1960
2013
|
}
|
|
1961
2014
|
}, 1e3);
|
package/package.json
CHANGED