diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index b5fc202..868592e 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,112 +1,34 @@
use tauri::{window::Color, WebviewUrl, WebviewWindowBuilder};
-#[tauri::command]
-fn navigate(webview_window: tauri::WebviewWindow, url: String) {
- _ = webview_window.navigate(tauri::Url::parse(&url).unwrap());
-}
-
-pub const ARKOSE_INIT_SCRIPT: &str = r#"
-if (window.location.origin === "https://client-api.arkoselabs.com") {
- const tweak = (obj, prop, val) => obj.__defineGetter__(prop, () => val)
-
- tweak(navigator, "hardwareConcurrency", 4)
- tweak(navigator, "userAgentData", null)
- tweak(navigator, "platform", "iPhone")
- tweak(navigator, "connection", null)
-
- tweak(window.screen, "height", 852)
- tweak(window.screen, "width", 393)
-
- tweak(window.screen, "availHeight", 852)
- tweak(window.screen, "availWidth", 393)
-
- tweak(window.screen, "pixelDepth", 24)
- tweak(window.screen, "colorDepth", 24)
-
- tweak(window.screen.orientation, "type", "portrait-primary")
- tweak(window, "devicePixelRatio", 3)
-
- tweak(window, "outerHeight", 0)
- tweak(window, "outerWidth", 0)
-
- const originalWebGLParameter = WebGLRenderingContext.prototype.getParameter;
- WebGLRenderingContext.prototype.getParameter = function (pname) {
- switch (pname) {
- case 0x9245: // UNMASKED_VENDOR_WEBGL
- return "Apple Inc.";
- case 0x9246: // UNMASKED_RENDERER_WEBGL
- return "Apple GPU";
- case 0x1F01: // RENDERER
- return "WebKit WebGL";
- case 0x1F00: // VENDOR
- return "WebKit";
- case 0x1F02: // VERSION
- return "WebGL 1.0";
- case 0x8B8C: // SHADING_LANGUAGE_VERSION
- return "WebGL GLSL ES 1.0 (1.0)";
- }
-
- return originalWebGLParameter.call(this, pname);
- }
-
- const originalWebGLAttributes = WebGLRenderingContext.prototype.getContextAttributes;
- WebGLRenderingContext.prototype.getContextAttributes = function () {
- const attributes = originalWebGLAttributes.call(this) ?? {};
- attributes.antialias = true;
- return attributes;
- }
-
- WebGLRenderingContext.prototype.getSupportedExtensions = function () {
- return [
- "ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_clip_control",
- "EXT_color_buffer_half_float", "EXT_depth_clamp", "EXT_float_blend",
- "EXT_frag_depth", "EXT_polygon_offset_clamp", "EXT_shader_texture_lod",
- "EXT_texture_compression_bptc", "EXT_texture_compression_rgtc",
- "EXT_texture_filter_anisotropic", "EXT_texture_mirror_clamp_to_edge",
- "EXT_sRGB", "KHR_parallel_shader_compile", "OES_element_index_uint",
- "OES_fbo_render_mipmap", "OES_standard_derivatives", "OES_texture_float",
- "OES_texture_float_linear", "OES_texture_half_float",
- "OES_texture_half_float_linear", "OES_vertex_array_object",
- "WEBGL_blend_func_extended", "WEBGL_color_buffer_float",
- "WEBGL_compressed_texture_astc", "WEBGL_compressed_texture_etc",
- "WEBGL_compressed_texture_etc1", "WEBGL_compressed_texture_pvrtc",
- "WEBKIT_WEBGL_compressed_texture_pvrtc", "WEBGL_compressed_texture_s3tc",
- "WEBGL_compressed_texture_s3tc_srgb", "WEBGL_debug_renderer_info",
- "WEBGL_debug_shaders", "WEBGL_depth_texture", "WEBGL_draw_buffers",
- "WEBGL_lose_context", "WEBGL_multi_draw", "WEBGL_polygon_mode"
- ];
- }
-}
-"#;
-
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
- tauri::Builder::default()
- .plugin(tauri_plugin_opener::init())
- .plugin(tauri_plugin_dialog::init())
- .plugin(tauri_plugin_os::init())
- .plugin(tauri_plugin_http::init())
- .plugin(tauri_plugin_internal_api::init())
- .setup(|app| {
- let mut win = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
- .background_color(Color(0, 0, 0, 255))
- .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148")
- .initialization_script_for_all_frames(ARKOSE_INIT_SCRIPT);
-
- #[cfg(not(mobile))]
- {
- win = win
- .title("StayReal")
- .theme(Some(tauri::Theme::Dark))
- .inner_size(436.0, 800.0)
- .min_inner_size(436.0, 600.0)
- }
-
- win.build()?;
-
- Ok(())
- })
- .invoke_handler(tauri::generate_handler![navigate])
- .run(tauri::generate_context!())
- .expect("error while running tauri application");
+ tauri::Builder::default()
+ .plugin(tauri_plugin_opener::init())
+ .plugin(tauri_plugin_dialog::init())
+ .plugin(tauri_plugin_os::init())
+ .plugin(tauri_plugin_http::init())
+ .plugin(tauri_plugin_internal_api::init())
+ .setup(|app| {
+ let mut win = WebviewWindowBuilder::new(app, "main", WebviewUrl::App("index.html".into()))
+ .background_color(Color(0, 0, 0, 255))
+ .user_agent("Mozilla/5.0 (iPhone; CPU iPhone OS 19_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148");
+
+ #[cfg(not(mobile))]
+ {
+ win = win
+ .title("StayReal")
+ .theme(Some(tauri::Theme::Dark))
+ .inner_size(436.0, 800.0)
+ .min_inner_size(436.0, 600.0)
+ }
+
+ let webview_window = win.build()?;
+
+ #[cfg(all(debug_assertions, not(mobile)))]
+ webview_window.open_devtools();
+
+ Ok(())
+ })
+ .run(tauri::generate_context!())
+ .expect("error while running tauri application");
}
diff --git a/src/api/constants.ts b/src/api/constants.ts
index 5be91cf..386b741 100644
--- a/src/api/constants.ts
+++ b/src/api/constants.ts
@@ -2,16 +2,15 @@ import { hex } from '@scure/base';
import { createBeRealSignature } from "./core/signature";
export const BEREAL_IOS_BUNDLE_ID = "AlexisBarreyat.BeReal";
-export const BEREAL_IOS_VERSION = "4.24.0";
-export const BEREAL_IOS_BUILD = "20523";
+export const BEREAL_IOS_VERSION = "4.65.0";
+export const BEREAL_IOS_BUILD = "22654";
export const BEREAL_TIMEZONE = new Intl.DateTimeFormat().resolvedOptions().timeZone;
-export const BEREAL_ARKOSE_PUBLIC_KEY = "CCB0863E-D45D-42E9-A6C8-9E8544E8B17E";
export const BEREAL_HMAC_KEY = hex.decode('3536303337663461663232666236393630663363643031346532656337316233')
export const BEREAL_FIREBASE_KEY = "AIzaSyCgNTZt6gzPMh-2voYXOvrt_UR_gpGl83Q";
export const BEREAL_CLIENT_SECRET = "962D357B-B134-4AB6-8F53-BEA2B7255420";
export const BEREAL_PLATFORM = "iOS";
-export const BEREAL_PLATFORM_VERSION = "19.0";
+export const BEREAL_PLATFORM_VERSION = "26.2";
export const BEREAL_DEFAULT_HEADERS = (deviceID: string) => ({
"bereal-platform": BEREAL_PLATFORM,
diff --git a/src/api/requests/auth/vonage/data-exchange.ts b/src/api/requests/auth/vonage/data-exchange.ts
deleted file mode 100644
index ee1bf26..0000000
--- a/src/api/requests/auth/vonage/data-exchange.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { BEREAL_DEFAULT_HEADERS } from "~/api/constants";
-import { BeRealError } from "~/api/models/errors";
-import { fetch } from "@tauri-apps/plugin-http";
-
-export const postVonageDataExchange = async (inputs: {
- deviceID: string
- phoneNumber: string
-}): Promise => {
- const response = await fetch("https://auth-l7.bereal.com/api/vonage/data-exchange", {
- method: "POST",
- headers: {
- ...BEREAL_DEFAULT_HEADERS(inputs.deviceID),
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- phoneNumber: inputs.phoneNumber,
- }),
- });
-
- if (response.status !== 200)
- throw new BeRealError("Not able to get data exchange for this phone number, please check the phone number and try again.");
-
- const json = await response.json() as {
- dataExchange: string
- };
-
- return json.dataExchange;
-};
diff --git a/src/api/requests/auth/vonage/request.ts b/src/api/requests/auth/vonage/request.ts
index bb4fa06..2b58052 100644
--- a/src/api/requests/auth/vonage/request.ts
+++ b/src/api/requests/auth/vonage/request.ts
@@ -4,7 +4,6 @@ import { fetch } from "@tauri-apps/plugin-http";
export const VonageRequestCodeTokenIdentifier = {
RECAPTCHA: "RE",
- ARKOSE: "AR"
} as const;
export type VonageRequestCodeTokenIdentifier = typeof VonageRequestCodeTokenIdentifier[keyof typeof VonageRequestCodeTokenIdentifier];
diff --git a/src/components/arkose.tsx b/src/components/arkose.tsx
deleted file mode 100644
index 37b5b67..0000000
--- a/src/components/arkose.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { BEREAL_IOS_BUNDLE_ID, BEREAL_IOS_VERSION, BEREAL_PLATFORM_VERSION } from "~/api/constants";
-
-import { sha256 } from '@noble/hashes/sha2';
-import { bytesToHex, randomBytes } from '@noble/hashes/utils';
-
-const between = (min: number, max: number): number => {
- return Math.floor(Math.random() * (max - min + 1)) + min;
-};
-
-export const createArkoseURL = (key: string, dataExchange: string, deviceId: string) => {
- // After the challenge, we have to go back to our Tauri app.
- const callback = window.location.origin + window.location.pathname;
-
- // Timestamp as of right now to generate the biometric motion data.
- const timestamp = Math.floor(Date.now() / 1000);
-
- // @see https://appledb.dev/device/iPhone-15-Pro.html
- // Currently: iPhone 15 Pro
- const product = "iPhone16,1";
-
- const html = ``
- + ``
- + ``
- + ``;
- return "data:text/html;base64," + btoa(html);
-};
diff --git a/src/views/login.tsx b/src/views/login.tsx
index ba4e362..4c1b222 100644
--- a/src/views/login.tsx
+++ b/src/views/login.tsx
@@ -1,146 +1,175 @@
-import { createEffect, on, Show, type Component } from "solid-js";
-import { useNavigate, useSearchParams } from "@solidjs/router";
+import { Show, type Component } from "solid-js";
+import { useNavigate } from "@solidjs/router";
import { createStore } from "solid-js/store";
import { v4 as uuidv4 } from "uuid";
-import { vonage_request_code, VonageRequestCodeTokenIdentifier } from "~/api/requests/auth/vonage/request";
+import { vonage_request_code } from "~/api/requests/auth/vonage/request";
import { firebase_verify_custom_token } from "~/api/requests/auth/firebase/verify-custom-token";
import { vonage_verify_otp } from "~/api/requests/auth/vonage/verify";
import { grant_firebase } from "~/api/requests/auth/token";
-import { BEREAL_ARKOSE_PUBLIC_KEY } from "~/api/constants";
-import { createArkoseURL } from "~/components/arkose";
import auth from "~/stores/auth";
import { DEMO_ACCESS_TOKEN, DEMO_PHONE_NUMBER, DEMO_REFRESH_TOKEN } from "~/utils/demo";
-import MdiChevronLeft from '~icons/mdi/chevron-left'
-import { postVonageDataExchange } from "~/api/requests/auth/vonage/data-exchange";
+import MdiChevronLeft from "~icons/mdi/chevron-left";
import Otp from "~/components/otp";
-import MdiLoading from '~icons/mdi/loading'
+import MdiLoading from "~icons/mdi/loading";
import { BeRealError } from "~/api/models/errors";
-import { invoke } from "@tauri-apps/api/core";
import StayRealLogo from "~icons/stayreal/logo";
import { openUrl } from "@tauri-apps/plugin-opener";
+type LoginStep = "phone" | "otp";
+type LoginFlow =
+ | "idle"
+ | "requestingCode"
+ | "awaitingOtp"
+ | "verifyingOtp"
+ | "authenticated";
+
const LoginView: Component = () => {
const navigate = useNavigate();
- const [params, setParams] = useSearchParams<{ arkoseToken: string }>()
const [state, setState] = createStore({
- step: "phone" as ("phone" | "otp"),
- deviceID: localStorage.getItem("login__deviceID") || uuidv4(),
+ step: "phone" as LoginStep,
+ flow: "idle" as LoginFlow,
+ deviceID: uuidv4(),
error: null as string | null,
- loading: false,
- phoneNumber: localStorage.getItem("login__phoneNumber") || "",
+ phoneNumber: "",
requestID: "",
otp: "",
- })
+ });
- const runAuthentication = async (): Promise => {
- if (!state.phoneNumber) return;
+ const normalizedPhoneNumber = (): string => {
+ return state.phoneNumber.replace(/ /g, "");
+ };
+
+ const mapErrorMessage = (error: unknown): string => {
+ if (error instanceof BeRealError)
+ return error.message;
+
+ return "An error occurred while authenticating, please try again later.";
+ };
+
+ const setPhoneError = (message: string): void => {
+ setState({
+ flow: "idle",
+ error: message,
+ });
+ };
+
+ const setOtpError = (message: string): void => {
+ setState({
+ flow: "awaitingOtp",
+ error: message,
+ });
+ };
- // Make sure there's no whitespace in the phone number.
- const phoneNumber = state.phoneNumber.replace(/ /g, "");
+ const requestPhoneVerificationCode = async (phoneNumber: string): Promise => {
+ setState({
+ error: null,
+ flow: "requestingCode",
+ });
try {
- setState("loading", true);
-
- if (state.step === "phone") {
- let requestID: string;
-
- if (phoneNumber === DEMO_PHONE_NUMBER) {
- requestID = "demo";
- }
- else {
- if (!params.arkoseToken) {
- const dataExchange = await postVonageDataExchange({
- deviceID: state.deviceID,
- phoneNumber
- });
-
- // Save the state in `localStorage` to remember it
- // when we come back from the Arkose challenge.
- localStorage.setItem("login__phoneNumber", phoneNumber);
- localStorage.setItem("login__deviceID", state.deviceID);
-
- const url = createArkoseURL(BEREAL_ARKOSE_PUBLIC_KEY, dataExchange, state.deviceID);
- // For some odd reason, we can't use `location.href = url` here
- // so we're doing it through a Tauri command instead.
- return invoke("navigate", { url });
- }
-
- // We can safely remove the temporary state from localStorage.
- localStorage.removeItem("login__phoneNumber");
- localStorage.removeItem("login__deviceID");
-
- const token = params.arkoseToken;
- // One time use, we don't need it anymore.
- setParams({ arkoseToken: void 0 });
-
- requestID = await vonage_request_code({
- deviceID: state.deviceID,
- phoneNumber,
- tokens: [{
- identifier: VonageRequestCodeTokenIdentifier.ARKOSE,
- token,
- }]
- });
- }
-
- setState({ step: "otp", requestID });
- }
- else if (state.step === "otp") {
- if (phoneNumber === DEMO_PHONE_NUMBER) {
- await auth.save({
- deviceId: state.deviceID,
- accessToken: DEMO_ACCESS_TOKEN,
- refreshToken: DEMO_REFRESH_TOKEN(0)
- });
- }
- else {
- // fun fact: this should match `grant_firebase`'s `access_token` value
- const token = await vonage_verify_otp({
- requestID: state.requestID,
- deviceID: state.deviceID,
- otp: state.otp.trim()
- });
-
- const idToken = await firebase_verify_custom_token(token);
- const tokens = await grant_firebase({
- deviceID: state.deviceID,
- idToken
- });
-
- await auth.save({
- deviceId: state.deviceID,
- accessToken: tokens.access_token,
- refreshToken: tokens.refresh_token
- });
- }
-
- navigate("/feed/friends");
- }
+ const requestID = await vonage_request_code({
+ deviceID: state.deviceID,
+ phoneNumber,
+ tokens: [],
+ });
+
+ setState({
+ step: "otp",
+ flow: "awaitingOtp",
+ requestID,
+ otp: "",
+ error: null,
+ });
}
catch (error) {
- if (error instanceof BeRealError) {
- setState("error", error.message);
+ setPhoneError(mapErrorMessage(error));
+ }
+ };
+
+ const authenticateOtp = async (): Promise => {
+ const phoneNumber = normalizedPhoneNumber();
+ if (!phoneNumber || !state.requestID)
+ return;
+
+ setState({
+ error: null,
+ flow: "verifyingOtp",
+ });
+
+ try {
+ if (phoneNumber === DEMO_PHONE_NUMBER) {
+ await auth.save({
+ deviceId: state.deviceID,
+ accessToken: DEMO_ACCESS_TOKEN,
+ refreshToken: DEMO_REFRESH_TOKEN(0),
+ });
}
else {
- setState("error", "An error occurred while authenticating, please try again later.");
+ const token = await vonage_verify_otp({
+ requestID: state.requestID,
+ deviceID: state.deviceID,
+ otp: state.otp.trim(),
+ });
+
+ const idToken = await firebase_verify_custom_token(token);
+ const tokens = await grant_firebase({
+ deviceID: state.deviceID,
+ idToken,
+ });
+
+ await auth.save({
+ deviceId: state.deviceID,
+ accessToken: tokens.access_token,
+ refreshToken: tokens.refresh_token,
+ });
}
+
+ setState("flow", "authenticated");
+ navigate("/feed/friends");
}
- finally {
- setState("loading", false);
+ catch (error) {
+ setOtpError(mapErrorMessage(error));
}
};
- // If we're coming back from the Arkose challenge, we need to
- // re-run the authentication process.
- createEffect(on(() => params.arkoseToken, (token) => {
- if (token)
- runAuthentication();
- }));
+ const runAuthentication = async (): Promise => {
+ if (!state.phoneNumber)
+ return;
+
+ const phoneNumber = normalizedPhoneNumber();
+
+ if (state.step === "phone") {
+ if (phoneNumber === DEMO_PHONE_NUMBER) {
+ setState({
+ step: "otp",
+ flow: "awaitingOtp",
+ requestID: "demo",
+ otp: "",
+ error: null,
+ });
+
+ return;
+ }
+
+ await requestPhoneVerificationCode(phoneNumber);
+ return;
+ }
+
+ await authenticateOtp();
+ };
+
+ const isPhoneLoading = (): boolean => {
+ return state.flow === "requestingCode";
+ };
+
+ const isOtpLoading = (): boolean => {
+ return state.flow === "verifyingOtp" || state.flow === "authenticated";
+ };
return (
@@ -152,7 +181,10 @@ const LoginView: Component = () => {
onClick={() => {
setState({
step: "phone",
+ flow: "idle",
+ requestID: "",
otp: "",
+ error: null,
});
}}
>
@@ -171,7 +203,7 @@ const LoginView: Component = () => {
class="flex flex-col gap-4 h-full pb-6"
onSubmit={(event) => {
event.preventDefault();
- runAuthentication();
+ void runAuthentication();
}}
>
@@ -188,53 +220,50 @@ const LoginView: Component = () => {
By continuing, you agree that StayReal is not affiliated with BeReal and that you are using this service at your own risk.
-
+
You also agree to and and .
-
-
- {state.error}
-
-
-
-
+
{
setState("otp", code);
- runAuthentication();
+ void runAuthentication();
}}
/>
-
}>
-
- You're authenticating on the demonstration account, your OTP code is 123456
+ You're authenticating on the demonstration account, your OTP code is 123456
}>
@@ -244,6 +273,12 @@ const LoginView: Component = () => {
+
+
+ {state.error}
+
+
+