diff --git a/frontend/src/basic/Main.tsx b/frontend/src/basic/Main.tsx index b87d564a..344300d2 100644 --- a/frontend/src/basic/Main.tsx +++ b/frontend/src/basic/Main.tsx @@ -1,14 +1,15 @@ import React, { useContext } from 'react'; -import { MemoryRouter, BrowserRouter, Routes, Route } from 'react-router-dom'; -import { Box, Slide, Typography, styled } from '@mui/material'; +import { MemoryRouter, BrowserRouter } from 'react-router-dom'; +import { Box, Typography, styled } from '@mui/material'; import { type UseAppStoreType, AppContext, closeAll } from '../contexts/AppContext'; -import { RobotPage, MakerPage, BookPage, OrderPage, SettingsPage, NavBar, MainDialogs } from './'; +import { NavBar, MainDialogs } from './'; import RobotAvatar from '../components/RobotAvatar'; import Notifications from '../components/Notifications'; import { useTranslation } from 'react-i18next'; import { GarageContext, type UseGarageStoreType } from '../contexts/GarageContext'; +import Routes from './Routes'; const Router = window.NativeRobosats === undefined ? BrowserRouter : MemoryRouter; @@ -53,87 +54,7 @@ const Main: React.FC = () => { )} - - {['/robot/:token?', '/', ''].map((path, index) => { - return ( - -
- -
- - } - key={index} - /> - ); - })} - - -
- -
- - } - /> - - -
- -
- - } - /> - - -
- -
- - } - /> - - -
- -
- - } - /> -
+
diff --git a/frontend/src/basic/Routes.tsx b/frontend/src/basic/Routes.tsx new file mode 100644 index 00000000..f0ab7d20 --- /dev/null +++ b/frontend/src/basic/Routes.tsx @@ -0,0 +1,113 @@ +import React, { useContext, useEffect } from 'react'; +import { Routes as DomRoutes, Route, useNavigate } from 'react-router-dom'; +import { Box, Slide, Typography, styled } from '@mui/material'; +import { type UseAppStoreType, AppContext } from '../contexts/AppContext'; + +import { RobotPage, MakerPage, BookPage, OrderPage, SettingsPage } from '.'; +import { GarageContext, UseGarageStoreType } from '../contexts/GarageContext'; + +const Routes: React.FC = () => { + const navigate = useNavigate(); + const { garage } = useContext(GarageContext); + const { page, slideDirection } = useContext(AppContext); + + useEffect(() => { + window.addEventListener('navigateToPage', (event) => { + const orderId = event?.detail?.order_id; + const coordinator = event?.detail?.coordinator; + if (orderId && coordinator) { + const slot = garage.getSlotByOrder(coordinator, orderId); + if (slot?.token) { + garage.setCurrentSlot(slot?.token); + navigate(`/order/${coordinator}/${orderId}`); + } + } + }); + }, []); + + return ( + + {['/robot/:token?', '/', ''].map((path, index) => { + return ( + +
+ +
+ + } + key={index} + /> + ); + })} + + +
+ +
+ + } + /> + + +
+ +
+ + } + /> + + +
+ +
+ + } + /> + + +
+ +
+ + } + /> +
+ ); +}; + +export default Routes; diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts index 46dfd5ff..b850d1e5 100644 --- a/frontend/src/models/Federation.model.ts +++ b/frontend/src/models/Federation.model.ts @@ -8,6 +8,7 @@ import { defaultExchange, } from '.'; import defaultFederation from '../../static/federation.json'; +import { systemClient } from '../services/System'; import { getHost } from '../utils'; import { coordinatorDefaultValues } from './Coordinator.model'; import { updateExchangeInfo } from './Exchange.model'; @@ -90,9 +91,12 @@ export class Federation { }; updateUrl = async (origin: Origin, settings: Settings, hostUrl: string): Promise => { + const federationUrls = {}; for (const coor of Object.values(this.coordinators)) { coor.updateUrl(origin, settings, hostUrl); + federationUrls[coor.shortAlias] = coor.url; } + systemClient.setCookie('federation', JSON.stringify(federationUrls)); }; update = async (): Promise => { diff --git a/frontend/src/models/Garage.model.ts b/frontend/src/models/Garage.model.ts index a1f347a1..48a28a30 100644 --- a/frontend/src/models/Garage.model.ts +++ b/frontend/src/models/Garage.model.ts @@ -109,6 +109,18 @@ class Garage { this.triggerHook('onRobotUpdate'); }; + getSlotByOrder: (coordinator: string, orderID: number) => Slot | null = ( + coordinator, + orderID, + ) => { + return ( + Object.values(this.slots).find((slot) => { + const robot = slot.getRobot(coordinator); + return slot.activeShortAlias === coordinator && robot?.activeOrderId === orderID; + }) ?? null + ); + }; + // Robots createRobot: (token: string, shortAliases: string[], attributes: Record) => void = ( token, diff --git a/frontend/src/services/Native/index.d.ts b/frontend/src/services/Native/index.d.ts index 680c8427..1ba9b275 100644 --- a/frontend/src/services/Native/index.d.ts +++ b/frontend/src/services/Native/index.d.ts @@ -25,7 +25,13 @@ export interface NativeWebViewMessageHttp { export interface NativeWebViewMessageSystem { id?: number; category: 'system'; - type: 'init' | 'torStatus' | 'copyToClipboardString' | 'setCookie' | 'deleteCookie'; + type: + | 'init' + | 'torStatus' + | 'copyToClipboardString' + | 'setCookie' + | 'deleteCookie' + | 'navigateToPage'; key?: string; detail?: string; } diff --git a/frontend/src/services/Native/index.ts b/frontend/src/services/Native/index.ts index ad2d49a3..77e9f5d8 100644 --- a/frontend/src/services/Native/index.ts +++ b/frontend/src/services/Native/index.ts @@ -1,3 +1,4 @@ +import { redirect } from 'react-router-dom'; import { type NativeRobosatsPromise, type NativeWebViewMessage, @@ -45,6 +46,8 @@ class NativeRobosats { if (message.key !== undefined) { this.cookies[message.key] = String(message.detail); } + } else if (message.type === 'navigateToPage') { + window.dispatchEvent(new CustomEvent('navigateToPage', { detail: message?.detail })); } }; diff --git a/mobile/App.tsx b/mobile/App.tsx index 218e6b9f..59dca69d 100644 --- a/mobile/App.tsx +++ b/mobile/App.tsx @@ -8,6 +8,7 @@ import { name as app_name, version as app_version } from './package.json'; import TorModule from './native/TorModule'; import RoboIdentitiesModule from './native/RoboIdentitiesModule'; import NotificationsModule from './native/NotificationsModule'; +import SystemModule from './native/SystemModule'; const backgroundColors = { light: 'white', @@ -24,6 +25,13 @@ const App = () => { useEffect(() => { TorModule.start(); + DeviceEventEmitter.addListener('navigateToPage', (payload) => { + injectMessage({ + category: 'system', + type: 'navigateToPage', + detail: payload, + }); + }); DeviceEventEmitter.addListener('TorStatus', (payload) => { if (payload.torStatus === 'OFF') TorModule.restart(); injectMessage({ @@ -73,7 +81,9 @@ const App = () => { loadCookie('settings_mode'); loadCookie('settings_light_qr'); loadCookie('settings_network'); - loadCookie('settings_use_proxy'); + loadCookie('settings_use_proxy').then((useProxy) => { + SystemModule.useProxy(useProxy ?? 'true'); + }); loadCookie('garage_slots').then((slots) => { NotificationsModule.monitorOrders(slots ?? '{}'); injectMessageResolve(responseId); @@ -133,6 +143,13 @@ const App = () => { Clipboard.setString(data.detail); } else if (data.type === 'setCookie') { setCookie(data.key, data.detail); + if (data.key === 'federation') { + SystemModule.setFederation(data.detail ?? '{}'); + } else if (data.key === 'garage_slots') { + NotificationsModule.monitorOrders(data.detail ?? '{}'); + } else if (data.key === 'settings_use_proxy') { + SystemModule.useProxy(data.detail ?? 'true'); + } } else if (data.type === 'deleteCookie') { EncryptedStorage.removeItem(data.key); } diff --git a/mobile/android/app/src/main/java/com/robosats/MainActivity.java b/mobile/android/app/src/main/java/com/robosats/MainActivity.java index afc644a8..82a4688c 100644 --- a/mobile/android/app/src/main/java/com/robosats/MainActivity.java +++ b/mobile/android/app/src/main/java/com/robosats/MainActivity.java @@ -1,8 +1,10 @@ package com.robosats; import android.Manifest; +import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; +import android.util.Log; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; @@ -15,6 +17,10 @@ import com.facebook.react.ReactActivity; import com.facebook.react.ReactActivityDelegate; import com.facebook.react.ReactInstanceManager; import com.facebook.react.ReactRootView; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.modules.core.DeviceEventManagerModule; import com.robosats.workers.NotificationWorker; import java.util.concurrent.TimeUnit; @@ -25,14 +31,37 @@ public class MainActivity extends ReactActivity { protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + String coordinator = extras.getString("coordinator"); + String token = extras.getString("token"); + int order_id = extras.getInt("order_id", 0); + if (order_id > 0) { + navigateToPage(coordinator, order_id); + } + } + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_CODE_POST_NOTIFICATIONS); } else { // Permission already granted, schedule your work - schedulePeriodicTask(); + scheduleFirstNotificationTask(); + schedulePeriodicNotificationTask(); } } + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + if (intent != null) { + String coordinator = intent.getStringExtra("coordinator"); + int order_id = intent.getIntExtra("order_id", 0); + if (order_id > 0) { + navigateToPage(coordinator, order_id); + } + } + } + /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. @@ -57,7 +86,8 @@ public class MainActivity extends ReactActivity { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_CODE_POST_NOTIFICATIONS) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - schedulePeriodicTask(); + scheduleFirstNotificationTask(); + schedulePeriodicNotificationTask(); } else { // Permission denied, handle accordingly // Maybe show a message to the user explaining why the permission is necessary @@ -65,15 +95,39 @@ public class MainActivity extends ReactActivity { } } - private void schedulePeriodicTask() { -// // Trigger the WorkManager setup and enqueueing here -// PeriodicWorkRequest periodicWorkRequest = -// new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) -// .build(); -// -// WorkManager.getInstance(getApplicationContext()) -// .enqueueUniquePeriodicWork("RobosatsNotificationsWork", -// ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest); + private void navigateToPage(String coordinator, Integer order_id) { + ReactContext reactContext = getReactInstanceManager().getCurrentReactContext(); + if (reactContext != null) { + WritableMap payload = Arguments.createMap(); + payload.putString("coordinator", coordinator); + payload.putInt("order_id", order_id); + reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("navigateToPage", payload); + } + } + + private void scheduleFirstNotificationTask() { + OneTimeWorkRequest workRequest = + new OneTimeWorkRequest.Builder(NotificationWorker.class) + .setInitialDelay(20, TimeUnit.SECONDS) + .build(); + + WorkManager.getInstance(getApplicationContext()) + .enqueue(workRequest); + } + + private void schedulePeriodicNotificationTask() { + // Trigger the WorkManager setup and enqueueing here + PeriodicWorkRequest periodicWorkRequest = + new PeriodicWorkRequest.Builder(NotificationWorker.class, 15, TimeUnit.MINUTES) + .build(); + + WorkManager.getInstance(getApplicationContext()) + .enqueueUniquePeriodicWork("RobosatsNotificationsWork", + ExistingPeriodicWorkPolicy.KEEP, periodicWorkRequest); + } + + private void scheduleInitialTask() { OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(NotificationWorker.class) .setInitialDelay(25, TimeUnit.SECONDS) diff --git a/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java b/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java index e78e0d31..d3f5ca7e 100644 --- a/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java +++ b/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java @@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; import com.robosats.modules.NotificationsModule; import com.robosats.modules.RoboIdentitiesModule; +import com.robosats.modules.SystemModule; import com.robosats.modules.TorModule; import java.util.ArrayList; @@ -23,6 +24,7 @@ public class RobosatsPackage implements ReactPackage { ReactApplicationContext reactContext) { List modules = new ArrayList<>(); + modules.add(new SystemModule(reactContext)); modules.add(new TorModule(reactContext)); modules.add(new NotificationsModule(reactContext)); modules.add(new RoboIdentitiesModule(reactContext)); diff --git a/mobile/android/app/src/main/java/com/robosats/modules/SystemModule.java b/mobile/android/app/src/main/java/com/robosats/modules/SystemModule.java new file mode 100644 index 00000000..f6206368 --- /dev/null +++ b/mobile/android/app/src/main/java/com/robosats/modules/SystemModule.java @@ -0,0 +1,41 @@ +package com.robosats.modules; + +import android.content.SharedPreferences; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class SystemModule extends ReactContextBaseJavaModule { + public SystemModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "SystemModule"; + } + + @ReactMethod + public void useProxy(String use_proxy) { + String PREFS_NAME = "System"; + String KEY_DATA = "UsePoxy"; + SharedPreferences sharedPreferences = getReactApplicationContext().getSharedPreferences(PREFS_NAME, ReactApplicationContext.MODE_PRIVATE); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(KEY_DATA, use_proxy); + + editor.apply(); + } + @ReactMethod + public void setFederation(String use_proxy) { + String PREFS_NAME = "System"; + String KEY_DATA = "Federation"; + SharedPreferences sharedPreferences = getReactApplicationContext().getSharedPreferences(PREFS_NAME, ReactApplicationContext.MODE_PRIVATE); + + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(KEY_DATA, use_proxy); + + editor.apply(); + } +} diff --git a/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java b/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java index c94aa656..8e145554 100644 --- a/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java +++ b/mobile/android/app/src/main/java/com/robosats/modules/TorModule.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.util.Objects; import java.util.concurrent.TimeUnit; +import kotlin.UninitializedPropertyAccessException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; @@ -45,7 +46,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException { + public void sendRequest(String action, String url, String headers, String body, final Promise promise) throws JSONException, UninitializedPropertyAccessException { OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout .readTimeout(30, TimeUnit.SECONDS) // Set read timeout @@ -93,7 +94,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void getTorStatus() { + public void getTorStatus() throws UninitializedPropertyAccessException { String torState = TorKmpManager.INSTANCE.getTorKmpObject().getTorState().getState().name(); WritableMap payload = Arguments.createMap(); payload.putString("torStatus", torState); @@ -103,7 +104,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void isConnected() { + public void isConnected() throws UninitializedPropertyAccessException { String isConnected = String.valueOf(TorKmpManager.INSTANCE.getTorKmpObject().isConnected()); WritableMap payload = Arguments.createMap(); payload.putString("isConnected", isConnected); @@ -113,7 +114,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void isStarting() { + public void isStarting() throws UninitializedPropertyAccessException { String isStarting = String.valueOf(TorKmpManager.INSTANCE.getTorKmpObject().isStarting()); WritableMap payload = Arguments.createMap(); payload.putString("isStarting", isStarting); @@ -123,7 +124,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void stop() { + public void stop() throws UninitializedPropertyAccessException { TorKmpManager.INSTANCE.getTorKmpObject().getTorOperationManager().stopQuietly(); WritableMap payload = Arguments.createMap(); context @@ -132,9 +133,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void start() { - TorKmp torKmp = new TorKmp(context.getCurrentActivity().getApplication()); - TorKmpManager.INSTANCE.updateTorKmpObject(torKmp); + public void start() throws InterruptedException, UninitializedPropertyAccessException { TorKmpManager.INSTANCE.getTorKmpObject().getTorOperationManager().startQuietly(); WritableMap payload = Arguments.createMap(); context @@ -143,7 +142,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void restart() { + public void restart() throws UninitializedPropertyAccessException { TorKmp torKmp = new TorKmp(context.getCurrentActivity().getApplication()); TorKmpManager.INSTANCE.updateTorKmpObject(torKmp); TorKmpManager.INSTANCE.getTorKmpObject().getTorOperationManager().restartQuietly(); @@ -154,7 +153,7 @@ public class TorModule extends ReactContextBaseJavaModule { } @ReactMethod - public void newIdentity() { + public void newIdentity() throws UninitializedPropertyAccessException { TorKmpManager.INSTANCE.getTorKmpObject().newIdentity(context.getCurrentActivity().getApplication()); WritableMap payload = Arguments.createMap(); context diff --git a/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt b/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt index cc5115a6..9accff0c 100644 --- a/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt +++ b/mobile/android/app/src/main/java/com/robosats/tor/TorKmpManager.kt @@ -27,6 +27,7 @@ import io.matthewnelson.kmp.tor.manager.R import kotlinx.coroutines.* import java.net.InetSocketAddress import java.net.Proxy +import java.util.concurrent.ExecutionException class TorKmp(application : Application) { @@ -391,6 +392,7 @@ class TorKmp(application : Application) { object TorKmpManager { private lateinit var torKmp: TorKmp + @Throws(UninitializedPropertyAccessException::class) fun getTorKmpObject(): TorKmp { return torKmp } diff --git a/mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java b/mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java index a81d02f4..db0dc3db 100644 --- a/mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java +++ b/mobile/android/app/src/main/java/com/robosats/workers/NotificationWorker.java @@ -3,8 +3,10 @@ package com.robosats.workers; import android.app.Application; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import android.util.Log; @@ -14,7 +16,9 @@ import androidx.work.Worker; import androidx.work.WorkerParameters; import com.facebook.react.bridge.ReactApplicationContext; +import com.robosats.MainActivity; import com.robosats.R; +import com.robosats.tor.TorKmp; import com.robosats.tor.TorKmpManager; import org.json.JSONArray; @@ -22,9 +26,13 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.net.Proxy; import java.util.Iterator; +import java.util.Objects; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import kotlin.UninitializedPropertyAccessException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.OkHttpClient; @@ -33,8 +41,11 @@ import okhttp3.Response; public class NotificationWorker extends Worker { private static final String CHANNEL_ID = "robosats_notifications"; - private static final String PREFS_NAME = "Notifications"; - private static final String KEY_DATA = "Slots"; + private static final String PREFS_NAME_NOTIFICATION = "Notifications"; + private static final String PREFS_NAME_SYSTEM = "System"; + private static final String KEY_DATA_SLOTS = "Slots"; + private static final String KEY_DATA_PROXY = "UsePoxy"; + private static final String KEY_DATA_FEDERATION = "Federation"; public NotificationWorker(Context context, WorkerParameters params) { super(context, params); @@ -43,8 +54,10 @@ public class NotificationWorker extends Worker { @Override public Result doWork() { - SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(PREFS_NAME, ReactApplicationContext.MODE_PRIVATE); - String slotsJson = sharedPreferences.getString(KEY_DATA, null); + SharedPreferences sharedPreferences = + getApplicationContext() + .getSharedPreferences(PREFS_NAME_NOTIFICATION, ReactApplicationContext.MODE_PRIVATE); + String slotsJson = sharedPreferences.getString(KEY_DATA_SLOTS, null); try { assert slotsJson != null; @@ -54,34 +67,59 @@ public class NotificationWorker extends Worker { while (it.hasNext()) { String robotToken = it.next(); JSONObject slot = (JSONObject) slots.get(robotToken); - JSONObject robots = slot.getJSONObject("robots"); - String activeShortAlias = slot.getString("activeShortAlias"); - JSONObject coordinatorRobot = robots.getJSONObject(activeShortAlias); - String coordinator = "satstralia"; - fetchNotifications(coordinatorRobot, coordinator); + JSONObject coordinatorRobot; + String activeShortAlias; + try { + activeShortAlias = slot.getString("activeShortAlias"); + coordinatorRobot = robots.getJSONObject(activeShortAlias); + fetchNotifications(coordinatorRobot, activeShortAlias); + } catch (JSONException | InterruptedException e) { + Log.d("JSON error", String.valueOf(e)); + } } } catch (JSONException e) { - throw new RuntimeException(e); + Log.d("JSON error", String.valueOf(e)); } return Result.success(); } - private void fetchNotifications(JSONObject robot, String coordinator) throws JSONException { - OkHttpClient client = new OkHttpClient.Builder() - .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout - .readTimeout(30, TimeUnit.SECONDS) // Set read timeout - .proxy(TorKmpManager.INSTANCE.getTorKmpObject().getProxy()) - .build(); - Request.Builder requestBuilder = new Request.Builder().url("http://satstraoq35jffvkgpfoqld32nzw2siuvowanruindbfojowpwsjdgad.onion/api/notifications"); + private void fetchNotifications(JSONObject robot, String coordinator) throws JSONException, InterruptedException { + String token = robot.getString("tokenSHA256"); + SharedPreferences sharedPreferences = + getApplicationContext() + .getSharedPreferences(PREFS_NAME_SYSTEM, ReactApplicationContext.MODE_PRIVATE); + boolean useProxy = Objects.equals(sharedPreferences.getString(KEY_DATA_PROXY, null), "true"); + JSONObject federation = new JSONObject(sharedPreferences.getString(KEY_DATA_FEDERATION, "{}")); + long unix_time_millis = sharedPreferences.getLong(token, 0); + String url = federation.getString(coordinator) + "/api/notifications"; +// if (unix_time_millis > 0) { +// String last_created_at = String +// .valueOf(LocalDateTime.ofInstant(Instant.ofEpochMilli(unix_time_millis), ZoneId.of("UTC"))); +// url += "?created_at=" + last_created_at; +// } - requestBuilder.addHeader("Authorization", "Token " + robot.getString("tokenSHA256")); + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .connectTimeout(60, TimeUnit.SECONDS) // Set connection timeout + .readTimeout(30, TimeUnit.SECONDS); // Set read timeout + + if (useProxy) { + TorKmp tor = this.getTorKmp(); + builder.proxy(tor.getProxy()); + } + + OkHttpClient client = builder.build(); + Request.Builder requestBuilder = new Request.Builder().url(url); + + requestBuilder + .addHeader("Authorization", "Token " + token); requestBuilder.get(); Request request = requestBuilder.build(); client.newCall(request).enqueue(new Callback() { @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { + displayErrorNotification(); Log.d("RobosatsError", e.toString()); } @@ -98,15 +136,14 @@ public class NotificationWorker extends Worker { }); try { JSONArray results = new JSONArray(body); - for (int i = 0; i < results.length(); i++) { - SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(PREFS_NAME, ReactApplicationContext.MODE_PRIVATE); - JSONObject notification = results.getJSONObject(i); + if (results.length() > 0) { + JSONObject notification = results.getJSONObject(0); Integer order_id = notification.getInt("order_id"); - displayNotification(order_id, notification.getString("title"), coordinator); + displayOrderNotification(order_id, notification.getString("title"), coordinator); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(coordinator + order_id, String.valueOf(notification.getInt("created_at"))); + editor.putLong(token, System.currentTimeMillis()); editor.apply(); } } catch (JSONException e) { @@ -117,23 +154,75 @@ public class NotificationWorker extends Worker { } - private void displayNotification(Integer order_id, String message, String coordinator) { + private void displayOrderNotification(Integer order_id, String message, String coordinator) { NotificationManager notificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel(CHANNEL_ID, - "Robosats", + order_id.toString(), NotificationManager.IMPORTANCE_HIGH); notificationManager.createNotificationChannel(channel); + Intent intent = new Intent(this.getApplicationContext(), MainActivity.class); + intent.putExtra("coordinator", coordinator); + intent.putExtra("order_id", order_id); + PendingIntent pendingIntent = PendingIntent.getActivity(this.getApplicationContext(), 0, + intent, PendingIntent.FLAG_IMMUTABLE); + NotificationCompat.Builder builder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) .setContentTitle("Order #" + order_id) .setContentText(message) .setSmallIcon(R.mipmap.ic_launcher_round) - .setPriority(NotificationCompat.PRIORITY_DEFAULT); + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setContentIntent(pendingIntent) + .setAutoCancel(true); notificationManager.notify(order_id, builder.build()); } + + private void displayErrorNotification() { + NotificationManager notificationManager = (NotificationManager) + getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); + + NotificationChannel channel = new NotificationChannel(CHANNEL_ID, + "robosats_error", + NotificationManager.IMPORTANCE_HIGH); + notificationManager.createNotificationChannel(channel); + + NotificationCompat.Builder builder = + new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) + .setContentTitle("Connection Error") + .setContentText("There was an error while connecting to the Tor network.") + .setSmallIcon(R.mipmap.ic_launcher_round) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true); + + notificationManager.notify(0, builder.build()); + } + + private TorKmp getTorKmp() throws InterruptedException { + TorKmp torKmp; + try { + torKmp = TorKmpManager.INSTANCE.getTorKmpObject(); + } catch (UninitializedPropertyAccessException e) { + torKmp = new TorKmp((Application) this.getApplicationContext()); + } + + int retires = 0; + while (!torKmp.isConnected() && retires < 15) { + if (!torKmp.isStarting()) { + torKmp.getTorOperationManager().startQuietly(); + } + Thread.sleep(2000); + retires += 1; + } + + if (!torKmp.isConnected()) { + displayErrorNotification(); + } + + return torKmp; + } } diff --git a/mobile/native/NotificationsModule.ts b/mobile/native/NotificationsModule.ts index a06d38a2..05751a6c 100644 --- a/mobile/native/NotificationsModule.ts +++ b/mobile/native/NotificationsModule.ts @@ -3,6 +3,8 @@ const { NotificationsModule } = NativeModules; interface NotificationsModuleInterface { monitorOrders: (slotsJson: string) => void; + useProxy: (useProxy: string) => void; + setFederation: (federation: string) => void; } export default NotificationsModule as NotificationsModuleInterface; diff --git a/mobile/native/SystemModule.ts b/mobile/native/SystemModule.ts new file mode 100644 index 00000000..aba2a392 --- /dev/null +++ b/mobile/native/SystemModule.ts @@ -0,0 +1,9 @@ +import { NativeModules } from 'react-native'; +const { SystemModule } = NativeModules; + +interface SystemModuleInterface { + useProxy: (useProxy: string) => void; + setFederation: (federation: string) => void; +} + +export default SystemModule as SystemModuleInterface;