diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 1bc158d13..90cee64b8 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -24,7 +24,11 @@ const App = (): JSX.Element => {
- {(window.NativeRobosats === undefined && window.RobosatsClient === undefined )? : }
+ {window.NativeRobosats === undefined && window.RobosatsClient === undefined ? (
+
+ ) : (
+
+ )}
diff --git a/frontend/src/basic/Main.tsx b/frontend/src/basic/Main.tsx
index 328c0b66c..d78533d31 100644
--- a/frontend/src/basic/Main.tsx
+++ b/frontend/src/basic/Main.tsx
@@ -1,16 +1,17 @@
import React, { useContext } from 'react';
-import { MemoryRouter,HashRouter ,BrowserRouter, Routes, Route } from 'react-router-dom';
-import { Box, Slide, Typography, styled } from '@mui/material';
+import { MemoryRouter, HashRouter, 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';
-function getRouter() {
+const getRouter = (): any => {
if (window.NativeRobosats === undefined && window.RobosatsClient === undefined) {
return BrowserRouter;
} else if (window.RobosatsClient === 'desktop-app') {
@@ -18,7 +19,7 @@ function getRouter() {
} else {
return MemoryRouter;
}
-}
+};
const Router = getRouter();
const TestnetTypography = styled(Typography)({
@@ -38,7 +39,7 @@ const MainBox = styled(Box)((props) => ({
const Main: React.FC = () => {
const { t } = useTranslation();
- const { settings, page, slideDirection, setOpen, windowSize, navbarHeight } =
+ const { settings, page, setOpen, windowSize, navbarHeight } =
useContext(AppContext);
const { garage } = useContext(GarageContext);
@@ -62,88 +63,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 000000000..909a7e459
--- /dev/null
+++ b/frontend/src/basic/Routes.tsx
@@ -0,0 +1,114 @@
+import React, { useContext, useEffect } from 'react';
+import { Routes as DomRoutes, Route, useNavigate } from 'react-router-dom';
+import { Slide } from '@mui/material';
+import { type UseAppStoreType, AppContext } from '../contexts/AppContext';
+
+import { RobotPage, MakerPage, BookPage, OrderPage, SettingsPage } from '.';
+import { GarageContext, type 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) => {
+ console.log('navigateToPage', JSON.stringify(event));
+ const orderId: string = event?.detail?.order_id;
+ const coordinator: string = 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/components/OrderDetails/index.tsx b/frontend/src/components/OrderDetails/index.tsx
index a368df363..1f4437ecd 100644
--- a/frontend/src/components/OrderDetails/index.tsx
+++ b/frontend/src/components/OrderDetails/index.tsx
@@ -174,8 +174,8 @@ const OrderDetails = ({
const isBuyer = (order.type === 0 && order.is_maker) || (order.type === 1 && !order.is_maker);
const tradeFee = order.is_maker
- ? (coordinator.info?.maker_fee ?? 0)
- : (coordinator.info?.taker_fee ?? 0);
+ ? coordinator.info?.maker_fee ?? 0
+ : coordinator.info?.taker_fee ?? 0;
const defaultRoutingBudget = 0.001;
const btc_now = order.satoshis_now / 100000000;
const rate = Number(order.max_amount ?? order.amount) / btc_now;
diff --git a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx
index bccc035eb..43de3a8f4 100644
--- a/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx
+++ b/frontend/src/components/TradeBox/EncryptedChat/EncryptedTurtleChat/index.tsx
@@ -39,7 +39,7 @@ const audioPath =
window.NativeRobosats === undefined
? '/static/assets/sounds'
: 'file:///android_asset/Web.bundle/assets/sounds';
-
+
const EncryptedTurtleChat: React.FC = ({
order,
userNick,
diff --git a/frontend/src/contexts/AppContext.tsx b/frontend/src/contexts/AppContext.tsx
index bd543bf96..26bc3c367 100644
--- a/frontend/src/contexts/AppContext.tsx
+++ b/frontend/src/contexts/AppContext.tsx
@@ -76,20 +76,13 @@ const makeTheme = function (settings: Settings): Theme {
};
const getHostUrl = (network = 'mainnet'): string => {
- let host = '';
- let protocol = '';
- if(isDesktopRoboSats){
- host = defaultFederation.exp[network].onion;
- protocol = 'http:';
- }
- else if (window.NativeRobosats === undefined) {
+ let host = defaultFederation.exp[network].onion;
+ let protocol = 'http:';
+ if (window.NativeRobosats === undefined) {
host = getHost();
protocol = location.protocol;
- } else {
- host = defaultFederation.exp[network].onion;
- protocol = 'http:';
}
- const hostUrl = `${host}`;
+ const hostUrl = `${protocol}//${host}`;
return hostUrl;
};
diff --git a/frontend/src/models/Federation.model.ts b/frontend/src/models/Federation.model.ts
index d39b25ee6..295fc90f2 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';
@@ -105,9 +106,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 b1f35f6c7..07645be99 100644
--- a/frontend/src/models/Garage.model.ts
+++ b/frontend/src/models/Garage.model.ts
@@ -83,7 +83,7 @@ class Garage {
// Slots
getSlot: (token?: string) => Slot | null = (token) => {
const currentToken = token ?? this.currentSlot;
- return currentToken ? (this.slots[currentToken] ?? null) : null;
+ return currentToken ? this.slots[currentToken] ?? null : null;
};
deleteSlot: (token?: string) => void = (token) => {
@@ -104,6 +104,7 @@ class Garage {
const slot = this.getSlot(token);
if (attributes) {
if (attributes.copiedToken !== undefined) slot?.setCopiedToken(attributes.copiedToken);
+ this.save();
this.triggerHook('onRobotUpdate');
}
return slot;
@@ -111,9 +112,22 @@ class Garage {
setCurrentSlot: (currentSlot: string) => void = (currentSlot) => {
this.currentSlot = currentSlot;
+ this.save();
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,
@@ -174,6 +188,7 @@ class Garage {
Object.values(this.slots).forEach((slot) => {
slot.syncCoordinator(coordinator, this);
});
+ this.save();
};
}
diff --git a/frontend/src/services/Native/index.d.ts b/frontend/src/services/Native/index.d.ts
index 4d7a17cbb..31208dfd8 100644
--- a/frontend/src/services/Native/index.d.ts
+++ b/frontend/src/services/Native/index.d.ts
@@ -26,7 +26,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 ad2d49a34..5e8941a15 100644
--- a/frontend/src/services/Native/index.ts
+++ b/frontend/src/services/Native/index.ts
@@ -45,6 +45,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/frontend/static/locales/ca.json b/frontend/static/locales/ca.json
index f9a394f61..50c080c7e 100644
--- a/frontend/static/locales/ca.json
+++ b/frontend/static/locales/ca.json
@@ -250,7 +250,7 @@
"Tell us about a new feature or a bug": "Proposa funcionalitats o notifica errors",
"We are abandoning Telegram! Our old TG groups": "Estem deixant Telegram! Els nostres grups antics de TG",
"X Official Account": "Compte oficial a X",
- "#23": "Phrases in components/Dialogs/Coordinator.tsx",
+ "#24": "Phrases in components/Dialogs/Coordinator.tsx",
"...Opening on Nostr gateway. Pubkey copied!": "...Obrint la passarel·la Nostr. Clau pública copiada!",
"24h contracted volume": "Volum contractat en 24h",
"24h non-KYC bitcoin premium": "Prima de bitcoin sense KYC en 24h",
@@ -475,7 +475,7 @@
"You send via Lightning {{amount}} Sats (Approx)": "Tu envies via Lightning {{amount}} Sats (Approx)",
"You send via {{method}} {{amount}}": "Envies via {{method}} {{amount}}",
"{{price}} {{currencyCode}}/BTC - Premium: {{premium}}%": "{{price}} {{currencyCode}}/BTC - Prima: {{premium}}%",
- "#42": "Phrases in components/RobotInfo/index.tsx",
+ "#43": "Phrases in components/RobotInfo/index.tsx",
"Active order!": "Active order!",
"Claim": "Retirar",
"Claim Sats!": "Reclamar Sats!",
diff --git a/mobile/App.tsx b/mobile/App.tsx
index e29775734..383a77345 100644
--- a/mobile/App.tsx
+++ b/mobile/App.tsx
@@ -7,6 +7,8 @@ import EncryptedStorage from 'react-native-encrypted-storage';
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',
@@ -23,6 +25,14 @@ const App = () => {
useEffect(() => {
TorModule.start();
+ DeviceEventEmitter.addListener('navigateToPage', (payload) => {
+ window.navigateToPage = payload;
+ injectMessage({
+ category: 'system',
+ type: 'navigateToPage',
+ detail: payload,
+ });
+ });
DeviceEventEmitter.addListener('TorStatus', (payload) => {
if (payload.torStatus === 'OFF') TorModule.restart();
injectMessage({
@@ -54,6 +64,17 @@ const App = () => {
);
};
+ const onLoadEnd = () => {
+ if (window.navigateToPage) {
+ injectMessage({
+ category: 'system',
+ type: 'navigateToPage',
+ detail: window.navigateToPage,
+ });
+ window.navigateToPage = undefined;
+ }
+ };
+
const init = (responseId: string) => {
const loadCookie = async (key: string) => {
return await EncryptedStorage.getItem(key).then((value) => {
@@ -62,6 +83,7 @@ const App = () => {
webViewRef.current?.injectJavaScript(
`(function() {window.NativeRobosats?.loadCookie(${json});})();`,
);
+ return value;
}
});
};
@@ -71,8 +93,13 @@ const App = () => {
loadCookie('settings_mode');
loadCookie('settings_light_qr');
loadCookie('settings_network');
- loadCookie('settings_use_proxy');
- loadCookie('garage_slots').then(() => injectMessageResolve(responseId));
+ loadCookie('settings_use_proxy').then((useProxy) => {
+ SystemModule.useProxy(useProxy ?? 'true');
+ });
+ loadCookie('garage_slots').then((slots) => {
+ NotificationsModule.monitorOrders(slots ?? '{}');
+ injectMessageResolve(responseId);
+ });
};
const onCatch = (dataId: string, event: any) => {
@@ -128,6 +155,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);
}
@@ -185,6 +219,7 @@ const App = () => {
allowsLinkPreview={false}
renderLoading={() => }
onError={(syntheticEvent) => {syntheticEvent.type}}
+ onLoadEnd={() => setTimeout(onLoadEnd, 3000)}
/>
);
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index f76cd6995..8ef828f52 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,9 @@
+
+
+
+
0) {
+ navigateToPage(coordinator, order_id);
+ }
+ }
+ }
+
+ @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
@@ -25,6 +70,31 @@ protected ReactActivityDelegate createReactActivityDelegate() {
return new MainActivityDelegate(this, getMainComponentName());
}
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ if (requestCode == REQUEST_CODE_POST_NOTIFICATIONS) {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Intent serviceIntent = new Intent(getApplicationContext(), NotificationsService.class);
+ getApplicationContext().startService(serviceIntent);
+ } else {
+ // Permission denied, handle accordingly
+ // Maybe show a message to the user explaining why the permission is necessary
+ }
+ }
+ }
+
+ 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);
+ }
+ }
+
public static class MainActivityDelegate extends ReactActivityDelegate {
public MainActivityDelegate(ReactActivity activity, String mainComponentName) {
super(activity, mainComponentName);
diff --git a/mobile/android/app/src/main/java/com/robosats/NotificationsService.java b/mobile/android/app/src/main/java/com/robosats/NotificationsService.java
new file mode 100644
index 000000000..59046d182
--- /dev/null
+++ b/mobile/android/app/src/main/java/com/robosats/NotificationsService.java
@@ -0,0 +1,301 @@
+package com.robosats;
+
+import android.app.Application;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.robosats.tor.TorKmp;
+import com.robosats.tor.TorKmpManager;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.Iterator;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import kotlin.UninitializedPropertyAccessException;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+public class NotificationsService extends Service {
+ private Handler handler;
+ private Runnable periodicTask;
+ private static final String CHANNEL_ID = "robosats_notifications";
+ private static final int NOTIFICATION_ID = 76453;
+ private static final long INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
+ 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";
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ createNotificationChannel();
+ startForeground(NOTIFICATION_ID, buildServiceNotification());
+
+ handler = new Handler();
+ periodicTask = new Runnable() {
+ @Override
+ public void run() {
+ Log.d("NotificationsService", "Running periodic task");
+ executeBackgroundTask();
+ handler.postDelayed(periodicTask, INTERVAL_MS);
+ }
+ };
+
+ Log.d("NotificationsService", "Squeduling periodic task");
+ handler.postDelayed(periodicTask, 5000);
+ return START_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (handler != null && periodicTask != null) {
+ handler.removeCallbacks(periodicTask);
+ }
+
+ stopForeground(true);
+ }
+
+ private void createNotificationChannel() {
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ "Robosats",
+ NotificationManager.IMPORTANCE_DEFAULT
+ );
+ manager.createNotificationChannel(channel);
+ }
+
+ private void executeBackgroundTask() {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ executor.submit(this::checkNotifications);
+ executor.shutdown();
+ }
+
+ private Notification buildServiceNotification() {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Tor Notifications")
+ .setContentText("Running in the background every 5 minutes to check for notifications.")
+ .setSmallIcon(R.mipmap.ic_icon)
+ .setTicker("Robosats")
+ .setPriority(NotificationCompat.PRIORITY_MIN)
+ .setOngoing(true)
+ .setAutoCancel(false)
+ .setContentIntent(pendingIntent);
+
+ return builder.build();
+ }
+
+ public void checkNotifications() {
+ Log.d("NotificationsService", "checkNotifications");
+ SharedPreferences sharedPreferences =
+ getApplicationContext()
+ .getSharedPreferences(PREFS_NAME_NOTIFICATION, ReactApplicationContext.MODE_PRIVATE);
+ String slotsJson = sharedPreferences.getString(KEY_DATA_SLOTS, null);
+
+ try {
+ assert slotsJson != null;
+ JSONObject slots = new JSONObject(slotsJson);
+ Iterator it = slots.keys();
+
+ while (it.hasNext()) {
+ String robotToken = it.next();
+ JSONObject slot = (JSONObject) slots.get(robotToken);
+ JSONObject robots = slot.getJSONObject("robots");
+ JSONObject coordinatorRobot;
+ String shortAlias = slot.getString("activeShortAlias");
+ coordinatorRobot = robots.getJSONObject(shortAlias);
+ fetchNotifications(coordinatorRobot, shortAlias);
+ }
+ } catch (JSONException | InterruptedException e) {
+ Log.d("NotificationsService", "Error reading garage: " + e);
+ }
+ }
+
+ 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;
+ }
+
+ 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("NotificationsService", "Error fetching coordinator: " + e.toString());
+ }
+
+ @Override
+ public void onResponse(Call call, Response response) throws IOException {
+ String body = response.body() != null ? response.body().string() : "{}";
+ JSONObject headersJson = new JSONObject();
+ response.headers().names().forEach(name -> {
+ try {
+ headersJson.put(name, response.header(name));
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ try {
+ JSONArray results = new JSONArray(body);
+ for (int i=0; i < results.length(); i++) {
+ JSONObject notification = results.getJSONObject(i);
+ Integer order_id = notification.getInt("order_id");
+ String title = notification.getString("title");
+
+ if (title.isEmpty()) {
+ continue;
+ }
+
+ displayOrderNotification(order_id, title, coordinator);
+
+ long milliseconds;
+ try {
+ String created_at = notification.getString("created_at");
+ LocalDateTime datetime = LocalDateTime.parse(created_at, DateTimeFormatter.ISO_DATE_TIME);
+ milliseconds = datetime.toInstant(ZoneOffset.UTC).toEpochMilli() + 1000;
+ } catch (JSONException e) {
+ milliseconds = System.currentTimeMillis();
+ }
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putLong(token, milliseconds);
+ editor.apply();
+ break;
+ }
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ });
+
+ }
+
+ private void displayOrderNotification(Integer order_id, String message, String coordinator) {
+ NotificationManager notificationManager = (NotificationManager)
+ getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
+
+ 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_icon)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setContentIntent(pendingIntent)
+ .setAutoCancel(true);
+
+ notificationManager.notify(order_id, builder.build());
+ }
+
+ private void displayErrorNotification() {
+ NotificationManager notificationManager = (NotificationManager)
+ getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE);
+
+ 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_icon)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .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/android/app/src/main/java/com/robosats/RobosatsPackage.java b/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java
index 2eca00805..d3f5ca7e8 100644
--- a/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java
+++ b/mobile/android/app/src/main/java/com/robosats/RobosatsPackage.java
@@ -4,7 +4,9 @@
import com.facebook.react.bridge.NativeModule;
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;
@@ -22,7 +24,9 @@ public List createNativeModules(
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));
return modules;
diff --git a/mobile/android/app/src/main/java/com/robosats/modules/NotificationsModule.java b/mobile/android/app/src/main/java/com/robosats/modules/NotificationsModule.java
new file mode 100644
index 000000000..c0ac9163a
--- /dev/null
+++ b/mobile/android/app/src/main/java/com/robosats/modules/NotificationsModule.java
@@ -0,0 +1,30 @@
+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 NotificationsModule extends ReactContextBaseJavaModule {
+ public NotificationsModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ }
+
+ @Override
+ public String getName() {
+ return "NotificationsModule";
+ }
+
+ @ReactMethod
+ public void monitorOrders(String slots_json) {
+ String PREFS_NAME = "Notifications";
+ String KEY_DATA = "Slots";
+ SharedPreferences sharedPreferences = getReactApplicationContext().getSharedPreferences(PREFS_NAME, ReactApplicationContext.MODE_PRIVATE);
+
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString(KEY_DATA, slots_json);
+
+ editor.apply();
+ }
+}
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 000000000..f62063686
--- /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 1266b152b..8e1455543 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
@@ -1,5 +1,6 @@
package com.robosats.modules;
+import android.app.Application;
import android.util.Log;
import androidx.annotation.NonNull;
@@ -11,6 +12,7 @@
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
+import com.robosats.tor.TorKmp;
import com.robosats.tor.TorKmpManager;
import org.json.JSONException;
@@ -20,6 +22,7 @@
import java.util.Objects;
import java.util.concurrent.TimeUnit;
+import kotlin.UninitializedPropertyAccessException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
@@ -30,10 +33,11 @@
public class TorModule extends ReactContextBaseJavaModule {
- private TorKmpManager torKmpManager;
private ReactApplicationContext context;
public TorModule(ReactApplicationContext reactContext) {
context = reactContext;
+ TorKmp torKmpManager = new TorKmp((Application) context.getApplicationContext());
+ TorKmpManager.INSTANCE.updateTorKmpObject(torKmpManager);
}
@Override
@@ -42,11 +46,12 @@ public String getName() {
}
@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
- .proxy(torKmpManager.getProxy()).build();
+ .proxy(TorKmpManager.INSTANCE.getTorKmpObject().getProxy())
+ .build();
Request.Builder requestBuilder = new Request.Builder().url(url);
@@ -89,8 +94,8 @@ public void onResponse(Call call, Response response) throws IOException {
}
@ReactMethod
- public void getTorStatus() {
- String torState = torKmpManager.getTorState().getState().name();
+ public void getTorStatus() throws UninitializedPropertyAccessException {
+ String torState = TorKmpManager.INSTANCE.getTorKmpObject().getTorState().getState().name();
WritableMap payload = Arguments.createMap();
payload.putString("torStatus", torState);
context
@@ -99,8 +104,8 @@ public void getTorStatus() {
}
@ReactMethod
- public void isConnected() {
- String isConnected = String.valueOf(torKmpManager.isConnected());
+ public void isConnected() throws UninitializedPropertyAccessException {
+ String isConnected = String.valueOf(TorKmpManager.INSTANCE.getTorKmpObject().isConnected());
WritableMap payload = Arguments.createMap();
payload.putString("isConnected", isConnected);
context
@@ -109,8 +114,8 @@ public void isConnected() {
}
@ReactMethod
- public void isStarting() {
- String isStarting = String.valueOf(torKmpManager.isStarting());
+ public void isStarting() throws UninitializedPropertyAccessException {
+ String isStarting = String.valueOf(TorKmpManager.INSTANCE.getTorKmpObject().isStarting());
WritableMap payload = Arguments.createMap();
payload.putString("isStarting", isStarting);
context
@@ -119,8 +124,8 @@ public void isStarting() {
}
@ReactMethod
- public void stop() {
- torKmpManager.getTorOperationManager().stopQuietly();
+ public void stop() throws UninitializedPropertyAccessException {
+ TorKmpManager.INSTANCE.getTorKmpObject().getTorOperationManager().stopQuietly();
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
@@ -128,9 +133,8 @@ public void stop() {
}
@ReactMethod
- public void start() {
- torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
- torKmpManager.getTorOperationManager().startQuietly();
+ public void start() throws InterruptedException, UninitializedPropertyAccessException {
+ TorKmpManager.INSTANCE.getTorKmpObject().getTorOperationManager().startQuietly();
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
@@ -138,9 +142,10 @@ public void start() {
}
@ReactMethod
- public void restart() {
- torKmpManager = new TorKmpManager(context.getCurrentActivity().getApplication());
- torKmpManager.getTorOperationManager().restartQuietly();
+ public void restart() throws UninitializedPropertyAccessException {
+ TorKmp torKmp = new TorKmp(context.getCurrentActivity().getApplication());
+ TorKmpManager.INSTANCE.updateTorKmpObject(torKmp);
+ TorKmpManager.INSTANCE.getTorKmpObject().getTorOperationManager().restartQuietly();
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
@@ -148,8 +153,8 @@ public void restart() {
}
@ReactMethod
- public void newIdentity() {
- torKmpManager.newIdentity(context.getCurrentActivity().getApplication());
+ public void newIdentity() throws UninitializedPropertyAccessException {
+ TorKmpManager.INSTANCE.getTorKmpObject().newIdentity(context.getCurrentActivity().getApplication());
WritableMap payload = Arguments.createMap();
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
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 bed7fbc52..9accff0c4 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,8 +27,9 @@ import io.matthewnelson.kmp.tor.manager.R
import kotlinx.coroutines.*
import java.net.InetSocketAddress
import java.net.Proxy
+import java.util.concurrent.ExecutionException
-class TorKmpManager(application : Application) {
+class TorKmp(application : Application) {
private val TAG = "TorListener"
@@ -387,3 +388,16 @@ class TorKmpManager(application : Application) {
}
}
}
+
+object TorKmpManager {
+ private lateinit var torKmp: TorKmp
+
+ @Throws(UninitializedPropertyAccessException::class)
+ fun getTorKmpObject(): TorKmp {
+ return torKmp
+ }
+
+ fun updateTorKmpObject(newKmpObject: TorKmp) {
+ torKmp = newKmpObject
+ }
+}
diff --git a/mobile/android/app/src/main/jniLibs/x86_64/librobohash.so b/mobile/android/app/src/main/jniLibs/x86_64/librobohash.so
new file mode 100755
index 000000000..1e26077b4
Binary files /dev/null and b/mobile/android/app/src/main/jniLibs/x86_64/librobohash.so differ
diff --git a/mobile/android/app/src/main/jniLibs/x86_64/librobonames.so b/mobile/android/app/src/main/jniLibs/x86_64/librobonames.so
new file mode 100755
index 000000000..d6e1df920
Binary files /dev/null and b/mobile/android/app/src/main/jniLibs/x86_64/librobonames.so differ
diff --git a/mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png b/mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png
new file mode 100644
index 000000000..ab5e6f084
Binary files /dev/null and b/mobile/android/app/src/main/res/mipmap-hdpi/ic_icon.png differ
diff --git a/mobile/native/NotificationsModule.ts b/mobile/native/NotificationsModule.ts
new file mode 100644
index 000000000..05751a6cc
--- /dev/null
+++ b/mobile/native/NotificationsModule.ts
@@ -0,0 +1,10 @@
+import { NativeModules } from 'react-native';
+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/RoboIdentitiesModule.ts b/mobile/native/RoboIdentitiesModule.ts
index a5c3c83ea..e76c5336c 100644
--- a/mobile/native/RoboIdentitiesModule.ts
+++ b/mobile/native/RoboIdentitiesModule.ts
@@ -2,8 +2,8 @@ import { NativeModules } from 'react-native';
const { RoboIdentitiesModule } = NativeModules;
interface RoboIdentitiesModuleInterface {
- generateRoboname: (initialString: String) => Promise;
- generateRobohash: (initialString: String) => Promise;
+ generateRoboname: (initialString: string) => Promise;
+ generateRobohash: (initialString: string) => Promise;
}
export default RoboIdentitiesModule as RoboIdentitiesModuleInterface;
diff --git a/mobile/native/SystemModule.ts b/mobile/native/SystemModule.ts
new file mode 100644
index 000000000..aba2a3927
--- /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;
diff --git a/mobile/patch_modules/react-native-tor/android/build.gradle b/mobile/patch_modules/react-native-tor/android/build.gradle
deleted file mode 100644
index f3d97dd92..000000000
--- a/mobile/patch_modules/react-native-tor/android/build.gradle
+++ /dev/null
@@ -1,132 +0,0 @@
-buildscript {
- // Buildscript is evaluated before everything else so we can't use getExtOrDefault
- def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['Tor_kotlinVersion']
-
- repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
- }
-
- dependencies {
- classpath 'com.android.tools.build:gradle:3.2.1'
- // noinspection DifferentKotlinGradleVersion
- classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
- }
-}
-
-apply plugin: 'com.android.library'
-apply plugin: 'kotlin-android'
-
-def getExtOrDefault(name) {
- return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['Tor_' + name]
-}
-
-def getExtOrIntegerDefault(name) {
- return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['Tor_' + name]).toInteger()
-}
-
-android {
- compileSdkVersion getExtOrIntegerDefault('compileSdkVersion')
- buildToolsVersion getExtOrDefault('buildToolsVersion')
- defaultConfig {
- minSdkVersion rootProject.ext.has('minSdkVersion') ? rootProject.ext.minSdkVersion : 16
- targetSdkVersion getExtOrIntegerDefault('targetSdkVersion')
- versionCode 1
- versionName "1.0"
-
- }
-
- buildTypes {
- release {
- minifyEnabled false
- }
- }
- lintOptions {
- disable 'GradleCompatible'
- }
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_1_8
- targetCompatibility JavaVersion.VERSION_1_8
- }
-}
-
-repositories {
- google()
- mavenCentral()
- gradlePluginPortal()
-
- def found = false
- def defaultDir = null
- def androidSourcesName = 'React Native sources'
-
- if (rootProject.ext.has('reactNativeAndroidRoot')) {
- defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
- } else {
- defaultDir = new File(
- projectDir,
- '/../../../node_modules/react-native/android'
- )
- }
-
- if (defaultDir.exists()) {
- maven {
- url defaultDir.toString()
- name androidSourcesName
- }
-
- logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}")
- found = true
- } else {
- def parentDir = rootProject.projectDir
-
- 1.upto(5, {
- if (found) return true
- parentDir = parentDir.parentFile
-
- def androidSourcesDir = new File(
- parentDir,
- 'node_modules/react-native'
- )
-
- def androidPrebuiltBinaryDir = new File(
- parentDir,
- 'node_modules/react-native/android'
- )
-
- if (androidPrebuiltBinaryDir.exists()) {
- maven {
- url androidPrebuiltBinaryDir.toString()
- name androidSourcesName
- }
-
- logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}")
- found = true
- } else if (androidSourcesDir.exists()) {
- maven {
- url androidSourcesDir.toString()
- name androidSourcesName
- }
-
- logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}")
- found = true
- }
- })
- }
-
- if (!found) {
- throw new GradleException(
- "${project.name}: unable to locate React Native android sources. " +
- "Ensure you have you installed React Native as a dependency in your project and try again."
- )
- }
-}
-
-def kotlin_version = getExtOrDefault('kotlinVersion')
-
-dependencies {
- // noinspection GradleDynamicVersion
- api 'com.facebook.react:react-native:+'
- implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
- compileOnly files('libs/sifir_android.aar')
-}