diff --git a/android/app/build.gradle b/android/app/build.gradle index d2e57e779b..913abe3bc0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -133,7 +133,8 @@ dependencies { compile project(':react-native-camera') compile project(':react-native-status') compile project(':react-native-orientation') - compile(group: 'status-im', name: 'status-go', version: '0.1.0-201606301634-5d7b29', ext: 'aar') + //compile(name:'statusgo-android-16', ext:'aar') + compile(group: 'status-im', name: 'status-go', version: '0.1.0-201607011545-da53ec', ext: 'aar') compile fileTree(dir: "node_modules/realm/android/libs", include: ["*.jar"]) } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 42b5ee2517..05008651d2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -23,10 +23,9 @@ + android:exported="true"/> diff --git a/android/app/src/main/java/com/statusim/GethService.java b/android/app/src/main/java/com/statusim/GethService.java deleted file mode 100644 index 7a11e0e739..0000000000 --- a/android/app/src/main/java/com/statusim/GethService.java +++ /dev/null @@ -1,129 +0,0 @@ -package com.statusim; - -import android.app.Service; -import android.content.Intent; -import android.os.Handler; -import android.os.AsyncTask; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; -import android.support.annotation.Nullable; -import android.util.Log; -import android.os.Environment; - -import java.lang.ref.WeakReference; - -import com.github.status_im.status_go.Statusgo; - -import java.io.File; - -public class GethService extends Service { - - private static final String TAG = "GethService"; - - private static boolean isGethStarted = false; - private static boolean isGethInitialized = false; - private final Handler handler = new Handler(); - - static class IncomingHandler extends Handler { - - private final WeakReference service; - - IncomingHandler(GethService service) { - - this.service = new WeakReference(service); - } - - @Override - public void handleMessage(Message message) { - - GethService service = this.service.get(); - if (service != null) { - if (!service.handleMessage(message)) { - super.handleMessage(message); - } - } - } - } - - final Messenger serviceMessenger = new Messenger(new IncomingHandler(this)); - - protected class StartTask extends AsyncTask { - - public StartTask() { - } - - protected Void doInBackground(Void... args) { - startGeth(); - return null; - } - - protected void onPostExecute(Void results) { - onGethStarted(); - } - } - - protected void onGethStarted() { - Log.d(TAG, "Geth Service started"); - isGethStarted = true; - } - - protected void startGeth() { - Log.d(TAG, "Starting background Geth Service"); - - File extStore = Environment.getExternalStorageDirectory(); - - final String dataFolder = extStore.exists() ? - extStore.getAbsolutePath() : - getApplicationInfo().dataDir; - - new Thread(new Runnable() { - public void run() { - Statusgo.StartNode(dataFolder); - } - }).start(); - } - - public void signalEvent(String jsonEvent) { - - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return serviceMessenger.getBinder(); - } - - @Override - public void onCreate() { - super.onCreate(); - System.loadLibrary("statusgo"); - - if (!isGethInitialized) { - isGethInitialized = true; - new StartTask().execute(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - //TODO: stop geth - isGethStarted = false; - isGethInitialized = false; - Log.d(TAG, "Geth Service stopped !"); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - return Service.START_STICKY; - } - - protected boolean handleMessage(Message message) { - return false; - } - - public static boolean isRunning() { - return isGethInitialized; - } -} diff --git a/android/app/src/main/java/com/statusim/MainActivity.java b/android/app/src/main/java/com/statusim/MainActivity.java index 4524cb19cb..6c779cee3b 100644 --- a/android/app/src/main/java/com/statusim/MainActivity.java +++ b/android/app/src/main/java/com/statusim/MainActivity.java @@ -1,33 +1,24 @@ package com.statusim; import com.facebook.react.ReactActivity; +import com.statusim.geth.module.GethPackage; import io.realm.react.RealmReactPackage; import com.oblador.vectoricons.VectorIconsPackage; import com.facebook.react.ReactPackage; import com.facebook.react.shell.MainReactPackage; import com.rt2zz.reactnativecontacts.ReactNativeContacts; import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.os.Message; -import android.os.Messenger; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnCancelListener; -import android.content.ComponentName; -import android.content.ServiceConnection; import android.content.Intent; -import android.content.Context; import com.bitgo.randombytes.RandomBytesPackage; import com.BV.LinearGradient.LinearGradientPackage; import com.centaurwarchief.smslistener.SmsListener; import com.github.yamill.orientation.OrientationPackage; - -import android.util.Log; - import java.util.Arrays; import java.util.List; import java.util.Properties; @@ -36,64 +27,12 @@ import com.statusim.Jail.JailPackage; import com.lwansbrough.RCTCamera.*; import com.i18n.reactnativei18n.ReactNativeI18n; -import io.realm.react.RealmReactPackage; -import android.content.Intent; import android.content.res.Configuration; public class MainActivity extends ReactActivity { private static final String TAG = "MainActivity"; - /** - * Incoming message handler. Calls to its binder are sequential! - */ - protected final IncomingHandler handler = new IncomingHandler(); - - /** Flag indicating if the service is bound. */ - protected boolean isBound; - - /** Sends messages to the service. */ - protected Messenger serviceMessenger = null; - - /** Receives messages from the service. */ - protected Messenger clientMessenger = new Messenger(handler); - - class IncomingHandler extends Handler { - - @Override - public void handleMessage(Message message) { - boolean isClaimed = false; - Log.d(TAG, "!!!!!!!!!!!!!! Received Service Message !!!!!!!!!!!!!!"); - super.handleMessage(message); - } - } - - protected ServiceConnection serviceConnection = new ServiceConnection() { - - public void onServiceConnected(ComponentName className, IBinder service) { - // This is called when the connection with the service has been - // established, giving us the object we can use to - // interact with the service. We are communicating with the - // service using a Messenger, so here we get a client-side - // representation of that from the raw IBinder object. - serviceMessenger = new Messenger(service); - isBound = true; - onConnected(); - } - - public void onServiceDisconnected(ComponentName className) { - // This is called when the connection with the service has been - // unexpectedly disconnected -- that is, its process crashed. - serviceMessenger = null; - isBound = false; - Log.d(TAG, "!!!!!!!!!!!!!! Geth Service Disconnected !!!!!!!!!!!!!!"); - } - }; - - protected void onConnected() { - Log.d(TAG, "!!!!!!!!!!!!!! Geth Service Connected !!!!!!!!!!!!!!"); - } - protected void startStatus() { // Required because of crazy APN settings redirecting localhost (found in GB) Properties properties = System.getProperties(); @@ -130,25 +69,11 @@ public class MainActivity extends ReactActivity { }).create(); dialog.show(); } - Intent intent = new Intent(this, GethService.class); - if (!GethService.isRunning()) { - startService(intent); - } - if (serviceConnection != null && GethService.isRunning()) { - bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); - } - } @Override protected void onDestroy() { super.onDestroy(); - try { - unbindService(serviceConnection); - } - catch (Throwable t) { - Log.e(TAG, "Failed to unbind from the geth service", t); - } } /** @@ -186,7 +111,8 @@ public class MainActivity extends ReactActivity { new LinearGradientPackage(), new RCTCameraPackage(), new SmsListener(this), - new OrientationPackage(this) + new OrientationPackage(this), + new GethPackage() ); } diff --git a/android/app/src/main/java/com/statusim/geth/module/GethModule.java b/android/app/src/main/java/com/statusim/geth/module/GethModule.java new file mode 100644 index 0000000000..058ab03667 --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/module/GethModule.java @@ -0,0 +1,219 @@ +package com.statusim.geth.module; + +import android.app.Activity; +import android.os.Bundle; +import android.os.Message; +import com.facebook.react.bridge.*; +import com.statusim.geth.service.ConnectorHandler; +import com.statusim.geth.service.GethConnector; +import com.statusim.geth.service.GethMessages; +import com.statusim.geth.service.GethService; +import android.util.Log; + +import java.util.HashMap; +import java.util.UUID; + +public class GethModule extends ReactContextBaseJavaModule implements LifecycleEventListener, ConnectorHandler { + + private static final String TAG = "GethModule"; + + protected GethConnector geth = null; + protected String handlerIdentifier = createIdentifier(); + + protected HashMap startNodeCallbacks = new HashMap<>(); + protected HashMap createAccountCallbacks = new HashMap<>(); + protected HashMap addAccountCallbacks = new HashMap<>(); + protected HashMap unlockAccountCallbacks = new HashMap<>(); + + + public GethModule(ReactApplicationContext reactContext) { + super(reactContext); + reactContext.addLifecycleEventListener(this); + } + + @Override + public String getName() { + return "Geth"; + } + + @Override + public void onHostResume() { // Actvity `onResume` + + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + return; + } + if (geth == null) { + geth = new GethConnector(currentActivity, GethService.class); + geth.registerHandler(this); + } + geth.bindService(); + } + + @Override + public void onHostPause() { // Actvity `onPause` + + if (geth != null) { + geth.unbindService(); + } + } + + @Override + public void onHostDestroy() { // Actvity `onDestroy` + + if (geth != null) { + geth.stopNode(null); + } + } + + @Override + public String getID() { + + return handlerIdentifier; + } + + @Override + public void onConnectorConnected() { + } + + @Override + public void onConnectorDisconnected() { + } + + @Override + public boolean handleMessage(Message message) { + + Log.d(TAG, "Received message: " + message.toString()); + boolean isClaimed = true; + Bundle data = message.getData(); + String callbackIdentifier = data.getString(GethConnector.CALLBACK_IDENTIFIER); + Log.d(TAG, "callback identifier: " + callbackIdentifier); + Callback callback = null; + switch (message.what) { + case GethMessages.MSG_NODE_STARTED: + Log.d(TAG, "handle startNodeCallbacks size: " + startNodeCallbacks.size()); + callback = startNodeCallbacks.remove(callbackIdentifier); + if (callback != null) { + callback.invoke(true); + } else { + Log.d(TAG, "Could not find callback: " + callbackIdentifier); + } + break; + case GethMessages.MSG_NODE_STOPPED: + break; + case GethMessages.MSG_ACCOUNT_CREATED: + callback = createAccountCallbacks.remove(callbackIdentifier); + if (callback != null) { + callback.invoke(data.getString("data")); + } + break; + case GethMessages.MSG_ACCOUNT_ADDED: + callback = addAccountCallbacks.remove(callbackIdentifier); + if (callback != null) { + callback.invoke(null, "{ \"address\": \"" + data.getString("address") + "\"}"); + } + break; + case GethMessages.MSG_LOGGED_IN: + callback = unlockAccountCallbacks.remove(callbackIdentifier); + if (callback != null) { + callback.invoke(null, "{ \"result\": \"" + data.getString("result") + "\"}"); + } + break; + default: + isClaimed = false; + } + + return isClaimed; + } + + @ReactMethod + public void startNode(Callback callback) { + + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + callback.invoke("Activity doesn't exist"); + return; + } + + if (geth == null) { + callback.invoke("Geth connector is null"); + return; + } + + String callbackIdentifier = createIdentifier(); + Log.d(TAG, "Created callback identifier: " + callbackIdentifier); + startNodeCallbacks.put(callbackIdentifier, callback); + Log.d(TAG, "startNodeCallbacks size: " + startNodeCallbacks.size()); + + geth.startNode(callbackIdentifier); + } + + @ReactMethod + public void login(String address, String password, Callback callback) { + + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + callback.invoke("Activity doesn't exist"); + return; + } + + if (geth == null) { + callback.invoke("Geth connector is null"); + return; + } + + String callbackIdentifier = createIdentifier(); + unlockAccountCallbacks.put(callbackIdentifier, callback); + + geth.login(callbackIdentifier, address, password); + } + + @ReactMethod + public void createAccount(String password, Callback callback) { + + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + callback.invoke("Activity doesn't exist"); + return; + } + + if (geth == null) { + callback.invoke("Geth connector is null"); + return; + } + + String callbackIdentifier = createIdentifier(); + createAccountCallbacks.put(callbackIdentifier, callback); + + geth.createAccount(callbackIdentifier, password); + } + + @ReactMethod + public void addAccount(String privateKey, Callback callback) { + + Activity currentActivity = getCurrentActivity(); + + if (currentActivity == null) { + callback.invoke("Activity doesn't exist"); + return; + } + + if (geth == null) { + callback.invoke("Geth connector is null"); + return; + } + + String callbackIdentifier = createIdentifier(); + addAccountCallbacks.put(callbackIdentifier, callback); + geth.addAccount(callbackIdentifier, privateKey); + } + + protected String createIdentifier() { + return UUID.randomUUID().toString(); + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/statusim/geth/module/GethPackage.java b/android/app/src/main/java/com/statusim/geth/module/GethPackage.java new file mode 100644 index 0000000000..d7dc02df6f --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/module/GethPackage.java @@ -0,0 +1,33 @@ +package com.statusim.geth.module; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class GethPackage implements ReactPackage { + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new GethModule(reactContext)); + + return modules; + } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/statusim/geth/service/ConnectorHandler.java b/android/app/src/main/java/com/statusim/geth/service/ConnectorHandler.java new file mode 100644 index 0000000000..0df2737a97 --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/service/ConnectorHandler.java @@ -0,0 +1,12 @@ +package com.statusim.geth.service; + + +import android.os.Message; + +public interface ConnectorHandler { + + boolean handleMessage(Message message); + void onConnectorConnected(); + void onConnectorDisconnected(); + String getID(); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/statusim/geth/service/GethConnector.java b/android/app/src/main/java/com/statusim/geth/service/GethConnector.java new file mode 100644 index 0000000000..7083dff2d5 --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/service/GethConnector.java @@ -0,0 +1,110 @@ +package com.statusim.geth.service; + + +import android.content.Context; +import android.os.Bundle; +import android.os.Message; +import android.os.RemoteException; +import android.util.Log; + +public class GethConnector extends ServiceConnector { + + private static final String TAG = "GethConnector"; + + public static final String CALLBACK_IDENTIFIER = "callbackIdentifier"; + + public GethConnector(Context context, Class serviceClass) { + + super(context, serviceClass); + } + + public void startNode(String callbackIdentifier) { + + if (checkBound()) { + Message msg = createMessage(callbackIdentifier, GethMessages.MSG_START_NODE, null); + try { + serviceMessenger.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Exception sending message(startNode) to service: ", e); + } + } + } + + public void stopNode(String callbackIdentifier) { + + if (checkBound()) { + Message msg = createMessage(callbackIdentifier, GethMessages.MSG_STOP_NODE, null); + try { + serviceMessenger.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Exception sending message(stopNode) to service: ", e); + } + } + } + + public void login(String callbackIdentifier, String address, String password) { + + if (checkBound()) { + Bundle data = new Bundle(); + data.putString("address", address); + data.putString("password", password); + Message msg = createMessage(callbackIdentifier, GethMessages.MSG_LOGIN, data); + try { + serviceMessenger.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Exception sending message(unlockAccount) to service: ", e); + } + } + } + + public void createAccount(String callbackIdentifier, String password) { + + if (checkBound()) { + Bundle data = new Bundle(); + data.putString("password", password); + Message msg = createMessage(callbackIdentifier, GethMessages.MSG_CREATE_ACCOUNT, data); + try { + serviceMessenger.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Exception sending message(createAccount) to service: ", e); + } + } + } + + public void addAccount(String callbackIdentifier, String privateKey) { + + if (checkBound()) { + Bundle data = new Bundle(); + data.putString("privateKey", privateKey); + Message msg = createMessage(callbackIdentifier, GethMessages.MSG_ADD_ACCOUNT, data); + try { + serviceMessenger.send(msg); + } catch (RemoteException e) { + Log.e(TAG, "Exception sending message(addAccount) to service: ", e); + } + } + } + + + protected boolean checkBound() { + + if (!isBound) { + Log.d(TAG, "GethConnector not bound!"); + return false; + } + return true; + } + + protected Message createMessage(String callbackIdentifier, int idMessage, Bundle data) { + + Log.d(TAG, "Client messenger: " + clientMessenger.toString()); + Message msg = Message.obtain(null, idMessage, 0, 0); + msg.replyTo = clientMessenger; + if (data == null) { + data = new Bundle(); + } + data.putString(CALLBACK_IDENTIFIER, callbackIdentifier); + msg.setData(data); + return msg; + } +} diff --git a/android/app/src/main/java/com/statusim/geth/service/GethMessages.java b/android/app/src/main/java/com/statusim/geth/service/GethMessages.java new file mode 100644 index 0000000000..f6c3e24737 --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/service/GethMessages.java @@ -0,0 +1,56 @@ +package com.statusim.geth.service; + + +public class GethMessages { + + /** + * Start the node + */ + public static final int MSG_START_NODE = 1; + + /** + * Node started event + */ + public static final int MSG_NODE_STARTED = 2; + + /** + * Stop the node + */ + public static final int MSG_STOP_NODE = 3; + + /** + * Node stopped event + */ + public static final int MSG_NODE_STOPPED = 4; + + /** + * Unlock an account + */ + public static final int MSG_LOGIN = 5; + + /** + * Account unlocked event + */ + public static final int MSG_LOGGED_IN = 6; + + /** + * Create an account + */ + public static final int MSG_CREATE_ACCOUNT = 7; + + /** + * Account created event + */ + public static final int MSG_ACCOUNT_CREATED = 8; + + /** + * Add an account + */ + public static final int MSG_ADD_ACCOUNT = 9; + + /** + * Account added event + */ + public static final int MSG_ACCOUNT_ADDED = 10; + +} diff --git a/android/app/src/main/java/com/statusim/geth/service/GethService.java b/android/app/src/main/java/com/statusim/geth/service/GethService.java new file mode 100644 index 0000000000..9900b3ef8e --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/service/GethService.java @@ -0,0 +1,255 @@ +package com.statusim.geth.service; + +import android.app.Service; +import android.content.Intent; +import android.os.*; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.lang.ref.WeakReference; + +import com.github.status_im.status_go.Statusgo; + +import java.io.File; + +public class GethService extends Service { + + private static final String TAG = "GethService"; + + private static boolean isGethStarted = false; + private static boolean isGethInitialized = false; + private final Handler handler = new Handler(); + + private static String dataFolder; + + static class IncomingHandler extends Handler { + + private final WeakReference service; + + IncomingHandler(GethService service) { + + this.service = new WeakReference(service); + } + + @Override + public void handleMessage(Message message) { + + GethService service = this.service.get(); + if (service != null) { + if (!service.handleMessage(message)) { + super.handleMessage(message); + } + } + } + } + + final Messenger serviceMessenger = new Messenger(new IncomingHandler(this)); + + + public static void signalEvent(String jsonEvent) { + System.out.println("\n\n\nIT WOOOOOORKS1111!!!!!!\n\n\n"); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return serviceMessenger.getBinder(); + } + + @Override + public void onCreate() { + super.onCreate(); + System.loadLibrary("statusgoraw"); + System.loadLibrary("statusgo"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + //TODO: stop geth + stopNode(null); + isGethStarted = false; + isGethInitialized = false; + Log.d(TAG, "Geth Service stopped !"); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return Service.START_STICKY; + } + + protected boolean handleMessage(Message message) { + switch (message.what) { + + case GethMessages.MSG_START_NODE: + Log.d(TAG, "Received start node message." + message.toString()); + startNode(message); + break; + + case GethMessages.MSG_STOP_NODE: + stopNode(message); + break; + + case GethMessages.MSG_CREATE_ACCOUNT: + createAccount(message); + break; + + case GethMessages.MSG_ADD_ACCOUNT: + addAccount(message); + break; + + case GethMessages.MSG_LOGIN: + login(message); + break; + + default: + return false; + } + + return true; + } + + protected void startNode(Message message) { + if (!isGethInitialized) { + isGethInitialized = true; + Log.d(TAG, "Client messenger1: " + message.replyTo.toString()); + Bundle data = message.getData(); + String callbackIdentifier = data.getString(GethConnector.CALLBACK_IDENTIFIER); + Log.d(TAG, "Callback identifier: " + callbackIdentifier); + new StartTask(message.replyTo, callbackIdentifier).execute(); + } + } + + protected class StartTask extends AsyncTask { + + protected String callbackIdentifier; + protected Messenger messenger; + + public StartTask(Messenger messenger, String callbackIdentifier) { + this.messenger = messenger; + this.callbackIdentifier = callbackIdentifier; + } + + protected Void doInBackground(Void... args) { + startGeth(); + return null; + } + + protected void onPostExecute(Void results) { + onGethStarted(messenger, callbackIdentifier); + } + } + + protected void onGethStarted(Messenger messenger, String callbackIdentifier) { + Log.d(TAG, "Geth Service started"); + isGethStarted = true; + Message replyMessage = Message.obtain(null, GethMessages.MSG_NODE_STARTED, 0, 0, null); + Bundle replyData = new Bundle(); + Log.d(TAG, "Callback identifier: " + callbackIdentifier); + replyData.putString(GethConnector.CALLBACK_IDENTIFIER, callbackIdentifier); + replyMessage.setData(replyData); + sendReply(messenger, replyMessage); + } + + protected void startGeth() { + + + File extStore = Environment.getExternalStorageDirectory(); + + dataFolder = extStore.exists() ? + extStore.getAbsolutePath() + "/ethereum" : + getApplicationInfo().dataDir + "/ethereum"; + Log.d(TAG, "Starting background Geth Service in folder: " + dataFolder); + try { + final File newFile = new File(dataFolder); + newFile.mkdir(); + } catch (Exception e) { + Log.e(TAG, "error making folder: " + dataFolder, e); + } + + new Thread(new Runnable() { + public void run() { + + Statusgo.StartNode(dataFolder); + } + }).start(); + } + + protected void stopNode(Message message) { + // TODO: stop node + + createAndSendReply(message, GethMessages.MSG_NODE_STOPPED, null); + } + + protected void createAccount(Message message) { + Bundle data = message.getData(); + String password = data.getString("password"); + // TODO: remove second argument + Log.d(TAG, "Creating account: " + password + " - " + dataFolder); + String jsonData = Statusgo.CreateAccount(password); + Log.d(TAG, "Created account: " + jsonData); + + Bundle replyData = new Bundle(); + replyData.putString("data", jsonData); + createAndSendReply(message, GethMessages.MSG_ACCOUNT_CREATED, replyData); + } + + protected void addAccount(Message message) { + Bundle data = message.getData(); + String privateKey = data.getString("privateKey"); + String password = data.getString("password"); + // TODO: add account + //String address = Statusgo.doAddAccount(privateKey, password); + String address = "added account address"; + Log.d(TAG, "Added account: " + address); + + Bundle replyData = new Bundle(); + replyData.putString("address", address); + createAndSendReply(message, GethMessages.MSG_ACCOUNT_ADDED, replyData); + } + + protected void login(Message message) { + Bundle data = message.getData(); + String address = data.getString("address"); + String password = data.getString("password"); + // TODO: remove third argument + String result = Statusgo.Login(address, password); + Log.d(TAG, "Unlocked account: " + result); + + Bundle replyData = new Bundle(); + replyData.putString("result", result); + createAndSendReply(message, GethMessages.MSG_LOGGED_IN, replyData); + } + + public static boolean isRunning() { + return isGethInitialized; + } + + protected void createAndSendReply(Message message, int replyIdMessage, Bundle replyData) { + + if (message == null) { + return; + } + Message replyMessage = Message.obtain(null, replyIdMessage, 0, 0, message.obj); + if (replyData == null) { + replyData = new Bundle(); + } + Bundle data = message.getData(); + String callbackIdentifier = data.getString(GethConnector.CALLBACK_IDENTIFIER); + Log.d(TAG, "Callback identifier: " + callbackIdentifier); + replyData.putString(GethConnector.CALLBACK_IDENTIFIER, callbackIdentifier); + replyMessage.setData(replyData); + + sendReply(message.replyTo, replyMessage); + } + + protected void sendReply(Messenger messenger, Message message) { + try { + messenger.send(message); + + } catch (Exception e) { + + Log.e(TAG, "Exception sending message id: " + message.what, e); + } + } +} diff --git a/android/app/src/main/java/com/statusim/geth/service/ServiceConnector.java b/android/app/src/main/java/com/statusim/geth/service/ServiceConnector.java new file mode 100644 index 0000000000..f20e1fcf66 --- /dev/null +++ b/android/app/src/main/java/com/statusim/geth/service/ServiceConnector.java @@ -0,0 +1,149 @@ +package com.statusim.geth.service; + + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.*; + +import java.util.ArrayList; + +public class ServiceConnector { + + private static final String TAG = "ServiceConnector"; + + /** + * Incoming message handler. Calls to its binder are sequential! + */ + protected final IncomingHandler handler; + + /** + * Handler thread to avoid running on the main UI thread + */ + protected final HandlerThread handlerThread; + + /** Context of the activity from which this connector was launched */ + protected Context context; + + /** The class of the service we want to connect to */ + protected Class serviceClass; + + /** Flag indicating if the service is bound. */ + protected boolean isBound; + + /** Sends messages to the service. */ + protected Messenger serviceMessenger = null; + + /** Receives messages from the service. */ + protected Messenger clientMessenger = null; + + protected ArrayList handlers = new ArrayList<>(); + + /** Handles incoming messages from service. */ + class IncomingHandler extends Handler { + + public IncomingHandler(HandlerThread thread) { + + super(thread.getLooper()); + } + + @Override + public void handleMessage(Message message) { + + boolean isClaimed = false; + //if (message.obj != null) { + // String identifier = ((Bundle) message.obj).getString("identifier"); + //if (identifier != null) { + + for (ConnectorHandler handler : handlers) { + // if (identifier.equals(handler.getID())) { + isClaimed = handler.handleMessage(message); + // } + } + // } + //} + if (!isClaimed) { + super.handleMessage(message); + } + } + } + + /** + * Class for interacting with the main interface of the service. + */ + protected ServiceConnection serviceConnection = new ServiceConnection() { + + public void onServiceConnected(ComponentName className, IBinder service) { + + // This is called when the connection with the service has been + // established, giving us the object we can use to + // interact with the service. We are communicating with the + // service using a Messenger, so here we get a client-side + // representation of that from the raw IBinder object. + serviceMessenger = new Messenger(service); + isBound = true; + for (ConnectorHandler handler: handlers) { + handler.onConnectorConnected(); + } + } + + public void onServiceDisconnected(ComponentName className) { + + // This is called when the connection with the service has been + // unexpectedly disconnected -- that is, its process crashed. + serviceMessenger = null; + isBound = false; + for (ConnectorHandler handler: handlers) { + handler.onConnectorDisconnected(); + } + } + }; + + public ServiceConnector(Context context, Class serviceClass) { + + this.context = context; + this.serviceClass = serviceClass; + handlerThread = new HandlerThread("HandlerThread"); + handlerThread.start(); + handler = new IncomingHandler(handlerThread); + clientMessenger = new Messenger(handler); + } + + /** Bind to the service */ + public boolean bindService() { + + if (serviceConnection != null) { + Intent intent = new Intent(context, serviceClass); + context.getApplicationContext().startService(intent); + return context.getApplicationContext().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); + } else { + return false; + } + } + + /** Unbind from the service */ + public void unbindService() { + + if (isBound && serviceConnection != null) { + context.getApplicationContext().unbindService(serviceConnection); + isBound = false; +/* + Intent intent = new Intent(context, serviceClass); + context.getApplicationContext().stopService(intent); +*/ + } + } + + public void registerHandler(ConnectorHandler handler) { + + if (!handlers.contains(handler)) { + handlers.add(handler); + } + } + + public void removeHandler(ConnectorHandler handler) { + + handlers.remove(handler); + } +} diff --git a/android/app/src/main/res/drawable-hdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-hdpi/icon_ok_blue.png new file mode 100644 index 0000000000..66e62ce1a3 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-hdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..dc6068da44 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-hdpi/scan_blue.png b/android/app/src/main/res/drawable-hdpi/scan_blue.png new file mode 100644 index 0000000000..7ea6117534 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-mdpi/icon_ok_blue.png new file mode 100644 index 0000000000..8b6c5482cf Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-mdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..62a6bdebf4 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-mdpi/scan_blue.png b/android/app/src/main/res/drawable-mdpi/scan_blue.png new file mode 100644 index 0000000000..f154c2ee43 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-xhdpi/icon_ok_blue.png new file mode 100644 index 0000000000..df4ddc7541 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-xhdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..07c323e77e Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/scan_blue.png b/android/app/src/main/res/drawable-xhdpi/scan_blue.png new file mode 100644 index 0000000000..df46334a87 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-xxhdpi/icon_ok_blue.png new file mode 100644 index 0000000000..9e8661249d Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-xxhdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..513a2177a2 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/scan_blue.png b/android/app/src/main/res/drawable-xxhdpi/scan_blue.png new file mode 100644 index 0000000000..b89f57307c Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/scan_blue.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_ok_blue.png b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_blue.png new file mode 100644 index 0000000000..f8a9b938b4 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_blue.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/icon_ok_disabled.png b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_disabled.png new file mode 100644 index 0000000000..5f4358614b Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/icon_ok_disabled.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/scan_blue.png b/android/app/src/main/res/drawable-xxxhdpi/scan_blue.png new file mode 100644 index 0000000000..2365f04fc5 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/scan_blue.png differ diff --git a/project.clj b/project.clj index 4007f3100d..af80bbed96 100644 --- a/project.clj +++ b/project.clj @@ -3,14 +3,14 @@ :url "http://example.com/FIXME" :license {:name "Eclipse Public License" :url "http://www.eclipse.org/legal/epl-v10.html"} - :dependencies [[org.clojure/clojure "1.7.0"] - [org.clojure/clojurescript "1.7.170"] + :dependencies [[org.clojure/clojure "1.9.0-alpha7"] + [org.clojure/clojurescript "1.9.76"] [reagent "0.5.1" :exclusions [cljsjs/react]] [re-frame "0.6.0"] [prismatic/schema "1.0.4"] ^{:voom {:repo "git@github.com:status-im/status-lib.git" :branch "master"}} - [status-im/protocol "0.1.1-20160525_083359-g53ab2c2"] + [status-im/protocol "0.1.1-20160630_153846-gbf92f5f"] [natal-shell "0.1.6"] [com.andrewmcveigh/cljs-time "0.4.0"]] :plugins [[lein-cljsbuild "1.1.1"] diff --git a/src/status_im/accounts/handlers.cljs b/src/status_im/accounts/handlers.cljs new file mode 100644 index 0000000000..cfb6d32d9b --- /dev/null +++ b/src/status_im/accounts/handlers.cljs @@ -0,0 +1,45 @@ +(ns status-im.accounts.handlers + (:require [status-im.models.accounts :as accounts] + [re-frame.core :refer [register-handler after dispatch debug]] + [status-im.utils.logging :as log] + [status-im.components.react :refer [geth]] + [status-im.utils.types :refer [json->clj]] + [status-im.persistence.simple-kv-store :as kv] + [status-im.protocol.state.storage :as storage] + [clojure.string :as str])) + + +(defn save-account [_ [_ account]] + (accounts/save-accounts [account])) + +(register-handler :add-account + (-> (fn [db [_ {:keys [address] :as account}]] + (update db :accounts assoc address account)) + ((after save-account)))) + +(defn save-password [password] + (storage/put kv/kv-store :password password)) + +(defn account-created [result password] + (let [data (json->clj result) + public-key (:pubkey data) + address (:address data) + account {:public-key public-key + :address address}] + (log/debug "Created account: " result) + (when (not (str/blank? public-key)) + (do + (save-password password) + (dispatch [:login-account address password]) + (dispatch [:initialize-protocol account]) + (dispatch [:add-account account]))))) + +(register-handler :create-account + (-> (fn [db [_ password]] + (.createAccount geth password (fn [result] (account-created result password))) + db))) + +(register-handler :login-account + (-> (fn [db [_ address password]] + (.login geth address password (fn [result] (log/debug "Logged in account: " address result))) + db))) \ No newline at end of file diff --git a/src/status_im/android/core.cljs b/src/status_im/android/core.cljs index 63586efbb2..77174edd2b 100644 --- a/src/status_im/android/core.cljs +++ b/src/status_im/android/core.cljs @@ -84,8 +84,10 @@ (defn init [] (dispatch-sync [:initialize-db]) (dispatch [:initialize-crypt]) + (dispatch [:initialize-geth]) (dispatch [:initialize-chats]) - (dispatch [:initialize-protocol]) + ;protocol must be initialized after user enters password and we create account + ;(dispatch [:initialize-protocol]) (dispatch [:load-user-phone-number]) (dispatch [:load-contacts]) (dispatch [:init-console-chat]) diff --git a/src/status_im/chat/handlers.cljs b/src/status_im/chat/handlers.cljs index 05ab26564f..97df9c4281 100644 --- a/src/status_im/chat/handlers.cljs +++ b/src/status_im/chat/handlers.cljs @@ -20,6 +20,8 @@ [status-im.handlers.content-suggestions :refer [get-content-suggestions]] [status-im.utils.phone-number :refer [format-phone-number]] [status-im.utils.datetime :as time] + [status-im.components.react :refer [geth]] + [status-im.utils.logging :as log] [status-im.components.jail :as j] [status-im.utils.types :refer [json->clj]] [status-im.commands.utils :refer [generate-hiccup]])) @@ -361,6 +363,7 @@ (register-handler :save-password (fn [db [_ password]] + (dispatch [:create-account password]) (sign-up-service/save-password password) (assoc db :password-saved true))) @@ -472,7 +475,9 @@ :group-chat false :is-active true :timestamp (.getTime (js/Date.)) - :contacts [{:identity contcat-id}]}] + :contacts [{:identity contcat-id}] + :dapp-url nil + :dapp-hash nil}] (assoc db :new-chat chat))) (defn add-chat [{:keys [new-chat] :as db} [_ chat-id]] diff --git a/src/status_im/chat/views/response.cljs b/src/status_im/chat/views/response.cljs index eaad55d093..a0d8e876bc 100644 --- a/src/status_im/chat/views/response.cljs +++ b/src/status_im/chat/views/response.cljs @@ -14,7 +14,8 @@ [status-im.chat.styles.response :as st] [status-im.chat.styles.dragdown :as ddst] [status-im.components.animation :as anim] - [status-im.chat.suggestions-responder :as resp])) + [status-im.chat.suggestions-responder :as resp] + [status-im.chat.constants :as c])) (defn drag-icon [] [view st/drag-container @@ -58,7 +59,8 @@ (defn container-animation-logic [{:keys [to-value val]}] (let [to-value @to-value] - (anim/start (anim/spring val {:toValue to-value})))) + (when-not (= to-value (.-_value val)) + (anim/start (anim/spring val {:toValue to-value}))))) (defn container [response-height & children] (let [;; todo to-response-height, cur-response-height must be specific @@ -89,7 +91,7 @@ (when (seq suggestions) suggestions)) (defn response-view [] - (let [response-height (anim/create-value 0)] + (let [response-height (anim/create-value c/input-height)] [container response-height [request-info response-height] [response-suggestions-view] diff --git a/src/status_im/chat/views/staged_command.cljs b/src/status_im/chat/views/staged_command.cljs index 424367c56e..4930192bce 100644 --- a/src/status_im/chat/views/staged_command.cljs +++ b/src/status_im/chat/views/staged_command.cljs @@ -11,12 +11,15 @@ (dispatch [:unstage-command staged-command])) (defn simple-command-staged-view [staged-command] - (let [command (:command staged-command)] + (let [{:keys [type name] :as command} (:command staged-command)] [view st/staged-command-container [view st/staged-command-background [view st/staged-command-info-container [view (st/staged-command-text-container command) - [text {:style st/staged-command-text} (str "!" (:name command))]] + [text {:style st/staged-command-text} + (if (= :command type) + (str "!" name) + name)]] [touchable-highlight {:style st/staged-command-cancel :onPress #(cancel-command-input staged-command)} [image {:source res/icon-close-gray diff --git a/src/status_im/chat/views/suggestions.cljs b/src/status_im/chat/views/suggestions.cljs index ca2bd8d0fe..909641d829 100644 --- a/src/status_im/chat/views/suggestions.cljs +++ b/src/status_im/chat/views/suggestions.cljs @@ -98,7 +98,8 @@ (defn container-animation-logic [{:keys [to-value val]}] (when-let [to-value @to-value] - (anim/start (anim/spring val {:toValue to-value})))) + (when-not (= to-value (.-_value val)) + (anim/start (anim/spring val {:toValue to-value}))))) (defn container [h & elements] (let [;; todo to-response-height, cur-response-height must be specific diff --git a/src/status_im/components/animation.cljs b/src/status_im/components/animation.cljs index 74d3cb3f78..0176b6e360 100644 --- a/src/status_im/components/animation.cljs +++ b/src/status_im/components/animation.cljs @@ -17,6 +17,9 @@ (defn anim-sequence [animations] (.sequence animated (clj->js animations))) +(defn parallel [animations] + (.parallel animated (clj->js animations))) + (defn anim-delay [duration] (.delay animated duration)) diff --git a/src/status_im/components/react.cljs b/src/status_im/components/react.cljs index f931ce4e00..f180f463c2 100644 --- a/src/status_im/components/react.cljs +++ b/src/status_im/components/react.cljs @@ -4,6 +4,7 @@ [status-im.utils.utils :as u])) (def react (u/require "react-native")) +(def native-modules (.-NativeModules react)) (defn get-react-property [name] (aget react name)) @@ -78,3 +79,5 @@ (def dismiss-keyboard! (u/require "dismissKeyboard")) (def device-event-emitter (.-DeviceEventEmitter react)) (def orientation (u/require "react-native-orientation")) + +(def geth (.-Geth native-modules)) diff --git a/src/status_im/components/styles.cljs b/src/status_im/components/styles.cljs index aaa82f5a42..22f6705300 100644 --- a/src/status_im/components/styles.cljs +++ b/src/status_im/components/styles.cljs @@ -65,6 +65,10 @@ {:width 23 :height 22}) +(def icon-scan + {:width 18 + :height 18}) + (def icon-plus {:width 18 :height 18}) @@ -94,10 +98,8 @@ (def button-input-container {:flex 1 - :flexDirection :row - :height 50}) + :flexDirection :row}) (def button-input {:flex 1 - :flexDirection :column - :height 50}) + :flexDirection :column}) diff --git a/src/status_im/components/text_field/styles.cljs b/src/status_im/components/text_field/styles.cljs new file mode 100644 index 0000000000..582454b4d6 --- /dev/null +++ b/src/status_im/components/text_field/styles.cljs @@ -0,0 +1,40 @@ +(ns status-im.components.text-field.styles) + + +(def text-field-container + {:position :relative + :height 72 + :paddingTop 30 + :paddingBottom 7}) + +(def text-input + {:fontSize 16 + :height 34 + :lineHeight 34 + :paddingBottom 5 + :textAlignVertical :top}) + +(defn label [top font-size color] + {:position :absolute + :top top + :left 0 + :color color + :fontSize font-size + :backgroundColor :transparent}) + +(def label-float + {}) + +(defn underline-container [backgroundColor] + {:backgroundColor backgroundColor + :height 1 + :alignItems :center}) + +(defn underline [backgroundColor width] + {:backgroundColor backgroundColor + :height 1 + :width width}) + +(defn error-text [color] + {:color color + :fontSize 12}) diff --git a/src/status_im/components/text_field/view.cljs b/src/status_im/components/text_field/view.cljs new file mode 100644 index 0000000000..f3757ac1bc --- /dev/null +++ b/src/status_im/components/text_field/view.cljs @@ -0,0 +1,189 @@ +(ns status-im.components.text-field.view + (:require [clojure.string :as s] + [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [reagent.core :as r] + [status-im.components.react :refer [react + view + text + animated-text + animated-view + text-input + touchable-opacity]] + [status-im.components.text-field.styles :as st] + [status-im.i18n :refer [label]] + [status-im.components.animation :as anim] + [status-im.utils.logging :as log])) + + +(def config {:label-top 16 + :label-bottom 37 + :label-font-large 16 + :label-font-small 12 + :label-animation-duration 200}) + +(def default-props {:wrapperStyle {} + :inputStyle {} + :lineStyle {} + :labelColor "#838c93" + :lineColor "#0000001f" + :focusLineColor "#0000001f" + :errorColor "#d50000" + :onFocus #() + :onBlur #() + :onChangeText #() + :onChange #()}) + +(defn field-animation [{:keys [top to-top font-size to-font-size + line-width to-line-width]}] + (let [duration (:label-animation-duration config) + animation (anim/parallel [(anim/timing top {:toValue to-top + :duration duration}) + (anim/timing font-size {:toValue to-font-size + :duration duration}) + (anim/timing line-width {:toValue to-line-width + :duration duration})])] + (anim/start animation (fn [arg] + (when (.-finished arg) + (log/debug "Field animation finished")))))) + +; Invoked once before the component is mounted. The return value will be used +; as the initial value of this.state. +(defn get-initial-state [component] + {:has-focus false + :float-label? false + :label-top 0 + :label-font-size 0 + :line-width (anim/create-value 0) + :max-line-width 100}) + +; Invoked once, both on the client and server, immediately before the initial +; rendering occurs. If you call setState within this method, render() will see +; the updated state and will be executed only once despite the state change. +(defn component-will-mount [component] + (let [{:keys [value] :as props} (r/props component) + data {:label-top (anim/create-value (if (s/blank? value) + (:label-bottom config) + (:label-top config))) + :label-font-size (anim/create-value (if (s/blank? value) + (:label-font-large config) + (:label-font-small config))) + :float-label? (if (s/blank? value) false true)}] + (log/debug "component-will-mount") + (r/set-state component data))) + +; Invoked once, only on the client (not on the server), immediately after the +; initial rendering occurs. At this point in the lifecycle, you can access any +; refs to your children (e.g., to access the underlying DOM representation). +; The componentDidMount() method of child components is invoked before that of +; parent components. +(defn component-did-mount [component] + (let [props (r/props component)] + (log/debug "component-did-mount:"))) + +; Invoked when a component is receiving new props. This method is not called for +; the initial render. Use this as an opportunity to react to a prop transition +; before render() is called by updating the state using this.setState(). +; The old props can be accessed via this.props. Calling this.setState() within +; this function will not trigger an additional render. +(defn component-will-receive-props [component new-props] + (log/debug "component-will-receive-props: new-props=" new-props)) + +; Invoked before rendering when new props or state are being received. This method +; is not called for the initial render or when forceUpdate is used. Use this as +; an opportunity to return false when you're certain that the transition to the +; new props and state will not require a component update. +; If shouldComponentUpdate returns false, then render() will be completely skipped +; until the next state change. In addition, componentWillUpdate and +; componentDidUpdate will not be called. +(defn should-component-update [component next-props next-state] + (log/debug "should-component-update: " next-props next-state) + true) + +; Invoked immediately before rendering when new props or state are being received. +; This method is not called for the initial render. Use this as an opportunity +; to perform preparation before an update occurs. +(defn component-will-update [component next-props next-state] + (log/debug "component-will-update: " next-props next-state)) + +; Invoked immediately after the component's updates are flushed to the DOM. +; This method is not called for the initial render. Use this as an opportunity +; to operate on the DOM when the component has been updated. +(defn component-did-update [component prev-props prev-state] + (log/debug "component-did-update: " prev-props prev-state)) + +(defn on-focus [{:keys [component animation onFocus]}] + (do + (log/debug "input focused") + (r/set-state component {:has-focus true + :float-label? true}) + (field-animation animation) + (when onFocus (onFocus)))) + +(defn on-blur [{:keys [component value animation onBlur]}] + (do + (log/debug "Input blurred") + (r/set-state component {:has-focus false + :float-label? (if (s/blank? value) false true)}) + (when (s/blank? value) + (field-animation animation)) + (when onBlur (onBlur)))) + +(defn get-width [event] + (.-width (.-layout (.-nativeEvent event)))) + +(defn reagent-render [data children] + (let [component (r/current-component) + {:keys [has-focus + float-label? + label-top + label-font-size + line-width + max-line-width] :as state} (r/state component) + {:keys [wrapperStyle inputStyle lineColor focusLineColor + labelColor errorColor error label value onFocus onBlur + onChangeText onChange] :as props} (merge default-props (r/props component)) + lineColor (if error errorColor lineColor) + focusLineColor (if error errorColor focusLineColor) + labelColor (if (and error (not float-label?)) errorColor labelColor) + label (if error (str label " *") label)] + (log/debug "reagent-render: " data state) + [view (merge st/text-field-container wrapperStyle) + [animated-text {:style (st/label label-top label-font-size labelColor)} label] + [text-input {:style (merge st/text-input inputStyle) + :placeholder "" + :onFocus #(on-focus {:component component + :animation {:top label-top + :to-top (:label-top config) + :font-size label-font-size + :to-font-size (:label-font-small config) + :line-width line-width + :to-line-width max-line-width} + :onFocus onFocus}) + :onBlur #(on-blur {:component component + :value value + :animation {:top label-top + :to-top (:label-bottom config) + :font-size label-font-size + :to-font-size (:label-font-large config) + :line-width line-width + :to-line-width 0} + :onBlur onBlur}) + :onChangeText #(onChangeText %) + :onChange #(onChange %)} value] + [view {:style (st/underline-container lineColor) + :onLayout #(r/set-state component {:max-line-width (get-width %)})} + [animated-view {:style (st/underline focusLineColor line-width)}]] + [text {:style (st/error-text errorColor)} error]])) + +(defn text-field [data children] + (let [component-data {:get-initial-state get-initial-state + :component-will-mount component-will-mount + :component-did-mount component-did-mount + :component-will-receive-props component-will-receive-props + :should-component-update should-component-update + :component-will-update component-will-update + :component-did-update component-did-update + :display-name "text-field" + :reagent-render reagent-render}] + (log/debug "Creating text-field component: " data) + (r/create-class component-data))) \ No newline at end of file diff --git a/src/status_im/contacts/styles.cljs b/src/status_im/contacts/styles.cljs index 4da8803d96..64cb693300 100644 --- a/src/status_im/contacts/styles.cljs +++ b/src/status_im/contacts/styles.cljs @@ -157,7 +157,8 @@ (def contact-form-container {:flex 1 - :color :white}) + :color :white + :backgroundColor :white}) (def gradient-background {:position :absolute @@ -168,4 +169,14 @@ (def form-container {:marginLeft 16 - :margin-top 50}) + :margin-top 16}) + +(def address-explication-container + {:flex 1 + :margin-top 30 + :paddingLeft 16 + :paddingRight 16}) + +(def address-explication + {:textAlign :center + :color "#838c93de"}) diff --git a/src/status_im/contacts/validations.cljs b/src/status_im/contacts/validations.cljs new file mode 100644 index 0000000000..1e4d517682 --- /dev/null +++ b/src/status_im/contacts/validations.cljs @@ -0,0 +1,21 @@ +(ns status-im.contacts.validations + (:require [cljs.spec :as s] + [status-im.persistence.realm :as realm])) + +(defn unique-identity? [identity] + (println identity) + (not (realm/exists? :contacts :whisper-identity identity))) + +(defn valid-length? [identity] + (= 132 (count identity))) + +(s/def ::identity-length valid-length?) +(s/def ::unique-identity unique-identity?) +(s/def ::not-empty-string (s/and string? not-empty)) +(s/def ::name ::not-empty-string) +(s/def ::whisper-identity (s/and ::not-empty-string + ::unique-identity + ::identity-length)) + +(s/def ::contact (s/keys :req-un [::name ::whisper-identity] + :opt-un [::phone ::photo-path ::address])) diff --git a/src/status_im/contacts/views/new_contact.cljs b/src/status_im/contacts/views/new_contact.cljs index a45de304a6..2dbe6abc82 100644 --- a/src/status_im/contacts/views/new_contact.cljs +++ b/src/status_im/contacts/views/new_contact.cljs @@ -1,12 +1,14 @@ (ns status-im.contacts.views.new-contact (:require-macros [status-im.utils.views :refer [defview]]) (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [clojure.string :as str] [status-im.components.react :refer [view text text-input image linear-gradient touchable-highlight]] + [status-im.components.text-field.view :refer [text-field]] [status-im.utils.identicon :refer [identicon]] [status-im.components.toolbar :refer [toolbar]] [status-im.components.styles :refer [color-purple @@ -19,58 +21,60 @@ toolbar-title-text button-input-container button-input - white-form-text-input]] - [status-im.qr-scanner.views.import-button :refer [import-button]] + form-text-input]] + [status-im.qr-scanner.views.scan-button :refer [scan-button]] [status-im.i18n :refer [label]] + [cljs.spec :as s] + [status-im.contacts.validations :as v] [status-im.contacts.styles :as st])) - (def toolbar-title [view toolbar-title-container - [text {:style (merge toolbar-title-text {:color color-white})} - (label :t/new-contact)]]) + [text {:style toolbar-title-text} + (label :t/add-new-contact)]]) (defview contact-name-input [name] [] - [text-input - {:underlineColorAndroid color-white - :placeholderTextColor color-white - :style white-form-text-input - :autoFocus true - :placeholder (label :t/contact-name) - :onChangeText #(dispatch [:set-in [:new-contact :name] %])} - name]) + [text-field + {:error (if (str/blank? name) "" nil) + :errorColor "#7099e6" + :value name + :label (label :t/name) + :onChangeText #(dispatch [:set-in [:new-contact :name] %])}]) (defview contact-whisper-id-input [whisper-identity] - [view button-input-container - [text-input - {:underlineColorAndroid color-white - :placeholderTextColor color-white - :style (merge white-form-text-input button-input) - :autoFocus true - :placeholder (label :t/whisper-identity) - :onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])} - whisper-identity] - [import-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]]) + [] + (let [error (if (str/blank? whisper-identity) "" nil) + error (if (s/valid? ::v/whisper-identity whisper-identity) + error + "Please enter a valid address or scan a QR code")] + [view button-input-container + [text-field + {:error error + :errorColor "#7099e6" + :value whisper-identity + :wrapperStyle (merge button-input) + :label (label :t/address) + :onChangeText #(dispatch [:set-in [:new-contact :whisper-identity] %])}] + [scan-button #(dispatch [:scan-qr-code {:toolbar-title (label :t/new-contact)} :set-new-contact-from-qr])]])) (defview new-contact [] [{:keys [name whisper-identity phone-number] :as new-contact} [:get :new-contact]] - [view st/contact-form-container - [linear-gradient {:colors ["rgba(182, 116, 241, 1)" "rgba(107, 147, 231, 1)" "rgba(43, 171, 238, 1)"] - :start [0, 0] - :end [0.5, 1] - :locations [0, 0.8, 1] - :style st/gradient-background}] - - [toolbar {:background-color :transparent - :nav-action {:image {:source {:uri :icon_back_white} - :style icon-back} - :handler #(dispatch [:navigate-back])} - :custom-content toolbar-title - :action {:image {:source {:uri :icon_add} - :style icon-search} - :handler #(dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)])}}] - [view st/form-container - [contact-whisper-id-input whisper-identity] - [contact-name-input name]]]) + (let [valid-contact? (s/valid? ::v/contact new-contact)] + [view st/contact-form-container + [toolbar {:background-color :white + :nav-action {:image {:source {:uri :icon_back} + :style icon-back} + :handler #(dispatch [:navigate-back])} + :custom-content toolbar-title + :action {:image {:source {:uri (if valid-contact? + :icon_ok_blue + :icon_ok_disabled)} + :style icon-search} + :handler #(when valid-contact? (dispatch [:add-new-contact (merge {:photo-path (identicon whisper-identity)} new-contact)]))}}] + [view st/form-container + [contact-name-input name] + [contact-whisper-id-input whisper-identity]] + [view st/address-explication-container + [text {:style st/address-explication} (label :t/address-explication)]]])) diff --git a/src/status_im/db.cljs b/src/status_im/db.cljs index 08f3e8abf0..9d1cc6fa5d 100644 --- a/src/status_im/db.cljs +++ b/src/status_im/db.cljs @@ -11,6 +11,7 @@ ;; initial state of app-db (def app-db {:identity-password "replace-me-with-user-entered-password" :identity "me" + :accounts {} :contacts [] :contacts-ids #{} :selected-contacts #{} diff --git a/src/status_im/handlers.cljs b/src/status_im/handlers.cljs index 1c656f5514..ee070f1ceb 100644 --- a/src/status_im/handlers.cljs +++ b/src/status_im/handlers.cljs @@ -7,7 +7,9 @@ [status-im.protocol.state.storage :as storage] [status-im.utils.logging :as log] [status-im.utils.crypt :refer [gen-random-bytes]] + [status-im.components.react :refer [geth]] [status-im.utils.handlers :refer [register-handler] :as u] + [status-im.models.protocol :as protocol] status-im.chat.handlers status-im.chat.handlers.animation status-im.group-settings.handlers @@ -19,6 +21,7 @@ status-im.commands.handlers.loading status-im.commands.handlers.jail status-im.qr-scanner.handlers + status-im.accounts.handlers status-im.protocol.handlers status-im.chat.handlers.requests)) @@ -55,7 +58,9 @@ (register-handler :initialize-db (fn [_ _] (assoc app-db - :signed-up (storage/get kv/kv-store :signed-up)))) + :signed-up (storage/get kv/kv-store :signed-up) + :user-identity (protocol/stored-identity nil) + :password (storage/get kv/kv-store :password)))) (register-handler :initialize-crypt (u/side-effect! @@ -73,6 +78,20 @@ (.addEntropy (.. js/ecc -sjcl -random))) (dispatch [:crypt-initialized])))))))) +(defn node-started [db result] + (let [identity (:user-identity db) + password (:password db)] + (log/debug "Started Node: " result) + (when identity (do + (dispatch [:login-account (:address identity) password]) + (dispatch [:initialize-protocol identity]))))) + +(register-handler :initialize-geth + (u/side-effect! + (fn [db _] + (log/debug "Starting node") + (.startNode geth (fn [result] (node-started db result)))))) + (register-handler :crypt-initialized (u/side-effect! (fn [_ _] diff --git a/src/status_im/handlers/server.cljs b/src/status_im/handlers/server.cljs index 5d9674c1d7..565f6f90c0 100644 --- a/src/status_im/handlers/server.cljs +++ b/src/status_im/handlers/server.cljs @@ -1,17 +1,20 @@ (ns status-im.handlers.server (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] - [status-im.utils.utils :refer [log on-error http-post]] + [status-im.utils.utils :refer [on-error http-post]] [status-im.utils.logging :as log])) (defn sign-up [db phone-number handler] - ;(user-data/save-phone-number phone-number) - (http-post "sign-up" {:phone-number phone-number - :whisper-identity (get-in db [:user-identity :public])} - (fn [body] - (log body) - (handler))) - db) + (let [{:keys [public-key address] :as account} (get-in db [:user-identity])] + ;(user-data/save-phone-number phone-number) + (log/debug "signing up with public-key" public-key "and phone " phone-number) + (http-post "sign-up" {:phone-number phone-number + :whisper-identity public-key + :address address} + (fn [body] + (log/debug body) + (handler))) + db)) (defn sign-up-confirm [confirmation-code handler] diff --git a/src/status_im/models/accounts.cljs b/src/status_im/models/accounts.cljs new file mode 100644 index 0000000000..a9f333c3c7 --- /dev/null +++ b/src/status_im/models/accounts.cljs @@ -0,0 +1,22 @@ +(ns status-im.models.accounts + (:require [status-im.persistence.realm :as r])) + +(defn get-accounts [] + (-> (r/get-all :accounts) + r/collection->map)) + +(defn create-account [{:keys [address public-key] :as account}] + (->> account + (r/create :accounts))) + +(defn save-accounts [accounts] + (r/write #(mapv create-account accounts))) + + +;;;;;;;;;;;;;;;;;;;;---------------------------------------------- + +(defn accounts-list [] + (r/get-all :accounts)) + +(defn account-by-address [address] + (r/single-cljs (r/get-by-field :accounts :address address))) diff --git a/src/status_im/models/protocol.cljs b/src/status_im/models/protocol.cljs index abe2ad110f..441d433688 100644 --- a/src/status_im/models/protocol.cljs +++ b/src/status_im/models/protocol.cljs @@ -12,13 +12,11 @@ (assoc-in db db/protocol-initialized-path initialized?)) (defn update-identity [db identity] - (let [password (:identity-password db) - encrypted (password-encrypt password (to-edn-string identity))] - (s/put kv/kv-store :identity encrypted) + (let [identity-string (to-edn-string identity)] + (s/put kv/kv-store :identity identity-string) (assoc db :user-identity identity))) (defn stored-identity [db] - (let [encrypted (s/get kv/kv-store :identity) - password (:identity-password db)] - (when encrypted - (read-string (password-decrypt password encrypted))))) + (let [identity (s/get kv/kv-store :identity)] + (when identity + (read-string identity)))) diff --git a/src/status_im/persistence/realm.cljs b/src/status_im/persistence/realm.cljs index e0ee29a606..e9e9e1098d 100644 --- a/src/status_im/persistence/realm.cljs +++ b/src/status_im/persistence/realm.cljs @@ -15,6 +15,10 @@ :optional true} :photo-path {:type "string" :optinal true}}} + {:name :accounts + :primaryKey :address + :properties {:address "string" + :public-key "string"}} {:name :requests :properties {:message-id :string :chat-id :string diff --git a/src/status_im/profile/screen.cljs b/src/status_im/profile/screen.cljs index ffff2d2e47..09f2fc3d07 100644 --- a/src/status_im/profile/screen.cljs +++ b/src/status_im/profile/screen.cljs @@ -67,7 +67,7 @@ phone-number [:get :phone-number] email [:get :email] status [:get :status] - identity [:get-in [:user-identity :public]]] + identity [:get-in [:user-identity :public-key]]] [scroll-view {:style st/profile} [touchable-highlight {:style st/back-btn-touchable :on-press #(dispatch [:navigate-back])} diff --git a/src/status_im/protocol/handlers.cljs b/src/status_im/protocol/handlers.cljs index e6e8c8699c..9bb41936e0 100644 --- a/src/status_im/protocol/handlers.cljs +++ b/src/status_im/protocol/handlers.cljs @@ -18,8 +18,8 @@ (register-handler :initialize-protocol (u/side-effect! - (fn [db [_]] - (init-protocol (make-handler db))))) + (fn [db [_ account]] + (init-protocol account (make-handler db))))) (register-handler :protocol-initialized (fn [db [_ identity]] diff --git a/src/status_im/qr_scanner/styles.cljs b/src/status_im/qr_scanner/styles.cljs index 05198031b1..057bf186c8 100644 --- a/src/status_im/qr_scanner/styles.cljs +++ b/src/status_im/qr_scanner/styles.cljs @@ -74,3 +74,25 @@ :flexDirection :column :color color-white :margin-left 8}) + + +(def scan-button + {:position :absolute + :bottom 0 + :right 16 + :flex 1 + :height 50 + :alignItems :center}) + +(def scan-button-content + {:flex 1 + :flexDirection :row + :height 50 + :alignItems :center + :alignSelf :center}) + +(def scan-text + {:flex 1 + :flexDirection :column + :color "#7099e6" + :margin-left 8}) \ No newline at end of file diff --git a/src/status_im/qr_scanner/views/import-qr-button.cljs b/src/status_im/qr_scanner/views/import-button.cljs similarity index 100% rename from src/status_im/qr_scanner/views/import-qr-button.cljs rename to src/status_im/qr_scanner/views/import-button.cljs diff --git a/src/status_im/qr_scanner/views/scan-button.cljs b/src/status_im/qr_scanner/views/scan-button.cljs new file mode 100644 index 0000000000..9ed1fd39e5 --- /dev/null +++ b/src/status_im/qr_scanner/views/scan-button.cljs @@ -0,0 +1,23 @@ +(ns status-im.qr-scanner.views.scan-button + (:require-macros [status-im.utils.views :refer [defview]]) + (:require [re-frame.core :refer [subscribe dispatch dispatch-sync]] + [status-im.components.react :refer [view + text + image + touchable-highlight]] + [status-im.components.toolbar :refer [toolbar]] + [status-im.components.drawer.view :refer [drawer-view open-drawer]] + [status-im.components.styles :refer [icon-scan]] + [status-im.i18n :refer [label]] + [status-im.qr-scanner.styles :as st])) + + +(defview scan-button [handler] + [] + [view st/scan-button + [touchable-highlight + {:on-press handler} + [view st/scan-button-content + [image {:source {:uri :scan_blue} + :style icon-scan}] + [text {:style st/scan-text} (label :t/scan-qr)]]]]) \ No newline at end of file diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index 792dc4ebd5..c667275426 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -119,9 +119,12 @@ :You "You" ;new-contact + :add-new-contact "Add new contact" :import-qr "Import" - :contact-name "Contact Name" + :scan-qr "Scan QR" + :name "Name" :whisper-identity "Whisper Identity" + :address-explication "Maybe here should be some text explaining what an address is and where to look for it" ;login :recover-from-passphrase "Recover from passphrase"