mirror of
https://github.com/vernu/textbee.git
synced 2026-04-20 15:01:00 -04:00
Merge pull request #8 from vernu/feature/receive-sms
Receive Messages Feature
This commit is contained in:
14
README.md
14
README.md
@@ -11,7 +11,7 @@ from their application via a REST API. It utilizes android phones as SMS gateway
|
||||
## Usage
|
||||
|
||||
1. Go to [textbee.dev](https://textbee.dev) and register or login with your account
|
||||
2. Install the app on your android phone from [textbee.dev/android](https://textbee.dev/android)
|
||||
2. Install the app on your android phone from [dl.textbee.dev](https://dl.textbee.dev)
|
||||
3. Open the app and grant the permissions for SMS
|
||||
4. Go to [textbee.dev/dashboard](https://textbee.dev/dashboard) and click register device/ generate API Key
|
||||
5. Scan the QR code with the app or enter the API key manually
|
||||
@@ -23,10 +23,14 @@ from their application via a REST API. It utilizes android phones as SMS gateway
|
||||
const API_KEY = 'YOUR_API_KEY';
|
||||
const DEVICE_ID = 'YOUR_DEVICE_ID';
|
||||
|
||||
await axios.post(`https://api.textbee.dev/api/v1/gateway/devices/${DEVICE_ID}/sendSMS?apiKey=${API_KEY}`, {
|
||||
receivers: [ '+251912345678' ],
|
||||
smsBody: 'Hello World!',
|
||||
})
|
||||
await axios.post(`https://api.textbee.dev/api/v1/gateway/devices/${DEVICE_ID}/sendSMS`, {
|
||||
recipients: [ '+251912345678' ],
|
||||
message: 'Hello World!',
|
||||
}, {
|
||||
headers: {
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -10,10 +10,16 @@ android {
|
||||
applicationId "com.vernu.sms"
|
||||
minSdk 24
|
||||
targetSdk 32
|
||||
versionCode 9
|
||||
versionName "2.2.0"
|
||||
versionCode 10
|
||||
versionName "2.3.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// javaCompileOptions {
|
||||
// annotationProcessorOptions {
|
||||
// arguments["room.schemaLocation"] = "$projectDir/schemas"
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -46,4 +52,9 @@ dependencies {
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
|
||||
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'
|
||||
}
|
||||
|
||||
// def room_version = "2.4.2"
|
||||
// implementation "androidx.room:room-runtime:$room_version"
|
||||
// annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,17 @@
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.vernu.sms">
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||
<uses-permission android:name="android.permission.READ_SMS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS" />
|
||||
<uses-permission android:name="android.provider.Telephony.SMS_RECEIVED" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -21,8 +30,31 @@
|
||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service
|
||||
android:name=".services.StickyNotificationService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
<receiver
|
||||
android:name=".receivers.SMSBroadcastReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter
|
||||
android:priority="2147483647">
|
||||
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:enabled="true"
|
||||
android:name=".receivers.BootCompletedReceiver"
|
||||
android:exported="true"
|
||||
android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name="com.vernu.sms.activities.MainActivity"
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
33
android/app/src/main/java/com/vernu/sms/ApiManager.java
Normal file
33
android/app/src/main/java/com/vernu/sms/ApiManager.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.vernu.sms;
|
||||
|
||||
import com.vernu.sms.services.GatewayApiService;
|
||||
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class ApiManager {
|
||||
private static GatewayApiService apiService;
|
||||
|
||||
public static GatewayApiService getApiService() {
|
||||
if (apiService == null) {
|
||||
apiService = createApiService();
|
||||
}
|
||||
return apiService;
|
||||
}
|
||||
|
||||
private static GatewayApiService createApiService() {
|
||||
// OkHttpClient.Builder httpClient = new OkHttpClient.Builder();
|
||||
// HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
|
||||
// loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
// httpClient.addInterceptor(loggingInterceptor);
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(AppConstants.API_BASE_URL)
|
||||
// .client(httpClient.build())
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
apiService = retrofit.create(GatewayApiService.class);
|
||||
|
||||
return retrofit.create(GatewayApiService.class);
|
||||
}
|
||||
}
|
||||
19
android/app/src/main/java/com/vernu/sms/AppConstants.java
Normal file
19
android/app/src/main/java/com/vernu/sms/AppConstants.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.vernu.sms;
|
||||
|
||||
import android.Manifest;
|
||||
|
||||
public class AppConstants {
|
||||
public static final String API_BASE_URL = "https://api.textbee.dev/api/v1/";
|
||||
public static final String[] requiredPermissions = new String[]{
|
||||
Manifest.permission.SEND_SMS,
|
||||
Manifest.permission.READ_SMS,
|
||||
Manifest.permission.RECEIVE_SMS,
|
||||
Manifest.permission.READ_PHONE_STATE
|
||||
};
|
||||
public static final String SHARED_PREFS_DEVICE_ID_KEY = "DEVICE_ID";
|
||||
public static final String SHARED_PREFS_API_KEY_KEY = "API_KEY";
|
||||
public static final String SHARED_PREFS_GATEWAY_ENABLED_KEY = "GATEWAY_ENABLED";
|
||||
public static final String SHARED_PREFS_PREFERRED_SIM_KEY = "PREFERRED_SIM";
|
||||
public static final String SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY = "RECEIVE_SMS_ENABLED";
|
||||
public static final String SHARED_PREFS_TRACK_SENT_SMS_STATUS_KEY = "TRACK_SENT_SMS_STATUS";
|
||||
}
|
||||
53
android/app/src/main/java/com/vernu/sms/TextBeeUtils.java
Normal file
53
android/app/src/main/java/com/vernu/sms/TextBeeUtils.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.vernu.sms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.telephony.SubscriptionInfo;
|
||||
import android.telephony.SubscriptionManager;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.vernu.sms.services.StickyNotificationService;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class TextBeeUtils {
|
||||
public static boolean isPermissionGranted(Context context, String permission) {
|
||||
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static List<SubscriptionInfo> getAvailableSimSlots(Context context) {
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
SubscriptionManager subscriptionManager = SubscriptionManager.from(context);
|
||||
return subscriptionManager.getActiveSubscriptionInfoList();
|
||||
|
||||
}
|
||||
|
||||
public static void startStickyNotificationService(Context context) {
|
||||
|
||||
if(!isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
|
||||
return;
|
||||
}
|
||||
|
||||
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(notificationIntent);
|
||||
} else {
|
||||
context.startService(notificationIntent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void stopStickyNotificationService(Context context) {
|
||||
Intent notificationIntent = new Intent(context, StickyNotificationService.class);
|
||||
context.stopService(notificationIntent);
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,12 @@ package com.vernu.sms.activities;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.SubscriptionInfo;
|
||||
import android.telephony.SubscriptionManager;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -25,95 +19,57 @@ import android.widget.RadioGroup;
|
||||
import android.widget.Switch;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.firebase.messaging.FirebaseMessaging;
|
||||
import com.google.zxing.integration.android.IntentIntegrator;
|
||||
import com.google.zxing.integration.android.IntentResult;
|
||||
import com.vernu.sms.services.GatewayApiService;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.BuildConfig;
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private Context mContext;
|
||||
private Retrofit retrofit;
|
||||
private GatewayApiService gatewayApiService;
|
||||
|
||||
private Switch gatewaySwitch;
|
||||
private Switch gatewaySwitch, receiveSMSSwitch;
|
||||
private EditText apiKeyEditText, fcmTokenEditText;
|
||||
private Button registerDeviceBtn, grantSMSPermissionBtn, scanQRBtn;
|
||||
private ImageButton copyDeviceIdImgBtn;
|
||||
private TextView deviceBrandAndModelTxt, deviceIdTxt;
|
||||
|
||||
private RadioGroup defaultSimSlotRadioGroup;
|
||||
|
||||
private static final int SEND_SMS_PERMISSION_REQUEST_CODE = 0;
|
||||
private static final int SCAN_QR_REQUEST_CODE = 49374;
|
||||
|
||||
private static final String API_BASE_URL = "https://api.textbee.dev/api/v1/";
|
||||
private static final int PERMISSION_REQUEST_CODE = 0;
|
||||
private String deviceId = null;
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mContext = getApplicationContext();
|
||||
|
||||
retrofit = new Retrofit.Builder()
|
||||
.baseUrl(API_BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
gatewayApiService = retrofit.create(GatewayApiService.class);
|
||||
|
||||
deviceId = SharedPreferenceHelper.getSharedPreferenceString(mContext, "DEVICE_ID", "");
|
||||
|
||||
deviceId = SharedPreferenceHelper.getSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");
|
||||
setContentView(R.layout.activity_main);
|
||||
gatewaySwitch = findViewById(R.id.gatewaySwitch);
|
||||
receiveSMSSwitch = findViewById(R.id.receiveSMSSwitch);
|
||||
apiKeyEditText = findViewById(R.id.apiKeyEditText);
|
||||
fcmTokenEditText = findViewById(R.id.fcmTokenEditText);
|
||||
registerDeviceBtn = findViewById(R.id.registerDeviceBtn);
|
||||
grantSMSPermissionBtn = findViewById(R.id.grantSMSPermissionBtn);
|
||||
scanQRBtn = findViewById(R.id.scanQRButton);
|
||||
|
||||
|
||||
deviceBrandAndModelTxt = findViewById(R.id.deviceBrandAndModelTxt);
|
||||
deviceIdTxt = findViewById(R.id.deviceIdTxt);
|
||||
|
||||
copyDeviceIdImgBtn = findViewById(R.id.copyDeviceIdImgBtn);
|
||||
|
||||
defaultSimSlotRadioGroup = findViewById(R.id.defaultSimSlotRadioGroup);
|
||||
|
||||
|
||||
try {
|
||||
getAvailableSimSlots().forEach(subscriptionInfo -> {
|
||||
RadioButton radioButton = new RadioButton(mContext);
|
||||
radioButton.setText(subscriptionInfo.getDisplayName().toString());
|
||||
radioButton.setId(subscriptionInfo.getSubscriptionId());
|
||||
radioButton.setOnClickListener(view -> {
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, "PREFERED_SIM", subscriptionInfo.getSubscriptionId());
|
||||
});
|
||||
radioButton.setChecked(subscriptionInfo.getSubscriptionId() == SharedPreferenceHelper.getSharedPreferenceInt(mContext, "PREFERED_SIM", 0));
|
||||
defaultSimSlotRadioGroup.addView(radioButton);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Snackbar.make(defaultSimSlotRadioGroup.getRootView(), "Error: " + e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
Log.e("SIM_SLOT_ERROR", e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
deviceIdTxt.setText(deviceId);
|
||||
deviceBrandAndModelTxt.setText(Build.BRAND + " " + Build.MODEL);
|
||||
|
||||
@@ -123,14 +79,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
registerDeviceBtn.setText("Update");
|
||||
}
|
||||
|
||||
if (isSMSPermissionGranted(mContext) && isReadPhoneStatePermissionGranted(mContext)) {
|
||||
String[] missingPermissions = Arrays.stream(AppConstants.requiredPermissions).filter(permission -> !TextBeeUtils.isPermissionGranted(mContext, permission)).toArray(String[]::new);
|
||||
if (missingPermissions.length == 0) {
|
||||
grantSMSPermissionBtn.setEnabled(false);
|
||||
grantSMSPermissionBtn.setText("SMS Permission Granted");
|
||||
grantSMSPermissionBtn.setText("Permission Granted");
|
||||
renderAvailableSimOptions();
|
||||
} else {
|
||||
Snackbar.make(grantSMSPermissionBtn, "Please Grant Required Permissions to continue: " + Arrays.toString(missingPermissions), Snackbar.LENGTH_SHORT).show();
|
||||
grantSMSPermissionBtn.setEnabled(true);
|
||||
grantSMSPermissionBtn.setOnClickListener(view -> handleSMSRequestPermission(view));
|
||||
grantSMSPermissionBtn.setOnClickListener(this::handleRequestPermissions);
|
||||
}
|
||||
|
||||
// TextBeeUtils.startStickyNotificationService(mContext);
|
||||
|
||||
copyDeviceIdImgBtn.setOnClickListener(view -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("Device ID", deviceId);
|
||||
@@ -138,70 +99,120 @@ public class MainActivity extends AppCompatActivity {
|
||||
Snackbar.make(view, "Copied", Snackbar.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
apiKeyEditText.setText(SharedPreferenceHelper.getSharedPreferenceString(mContext, "API_KEY", ""));
|
||||
|
||||
gatewaySwitch.setChecked(SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, "GATEWAY_ENABLED", false));
|
||||
apiKeyEditText.setText(SharedPreferenceHelper.getSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, ""));
|
||||
gatewaySwitch.setChecked(SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, false));
|
||||
gatewaySwitch.setOnCheckedChangeListener((compoundButton, isCheked) -> {
|
||||
View view = compoundButton.getRootView();
|
||||
compoundButton.setEnabled(false);
|
||||
String key = apiKeyEditText.getText().toString();
|
||||
|
||||
|
||||
RegisterDeviceInputDTO registerDeviceInput = new RegisterDeviceInputDTO();
|
||||
registerDeviceInput.setEnabled(isCheked);
|
||||
registerDeviceInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
registerDeviceInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = gatewayApiService.updateDevice(deviceId, key, registerDeviceInput);
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().updateDevice(deviceId, key, registerDeviceInput);
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
Snackbar.make(view, "Gateway " + (isCheked ? "enabled" : "disabled"), Snackbar.LENGTH_LONG).show();
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, "GATEWAY_ENABLED", isCheked);
|
||||
compoundButton.setChecked(Boolean.TRUE.equals(response.body().data.get("enabled")));
|
||||
} else {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, response.message(), Snackbar.LENGTH_LONG).show();
|
||||
compoundButton.setEnabled(true);
|
||||
return;
|
||||
}
|
||||
Snackbar.make(view, "Gateway " + (isCheked ? "enabled" : "disabled"), Snackbar.LENGTH_LONG).show();
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_GATEWAY_ENABLED_KEY, isCheked);
|
||||
boolean enabled = Boolean.TRUE.equals(Objects.requireNonNull(response.body()).data.get("enabled"));
|
||||
compoundButton.setChecked(enabled);
|
||||
// if (enabled) {
|
||||
// TextBeeUtils.startStickyNotificationService(mContext);
|
||||
// } else {
|
||||
// TextBeeUtils.stopStickyNotificationService(mContext);
|
||||
// }
|
||||
compoundButton.setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Snackbar.make(view, "An error occured :(", Snackbar.LENGTH_LONG).show();
|
||||
compoundButton.setEnabled(true);
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
registerDeviceBtn.setOnClickListener(view -> handleRegisterDevice());
|
||||
receiveSMSSwitch.setChecked(SharedPreferenceHelper.getSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY, false));
|
||||
receiveSMSSwitch.setOnCheckedChangeListener((compoundButton, isCheked) -> {
|
||||
View view = compoundButton.getRootView();
|
||||
SharedPreferenceHelper.setSharedPreferenceBoolean(mContext, AppConstants.SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY, isCheked);
|
||||
compoundButton.setChecked(isCheked);
|
||||
Snackbar.make(view, "Receive SMS " + (isCheked ? "enabled" : "disabled"), Snackbar.LENGTH_LONG).show();
|
||||
});
|
||||
|
||||
// TODO: check gateway status/api key/device validity and update UI accordingly
|
||||
registerDeviceBtn.setOnClickListener(view -> handleRegisterDevice());
|
||||
scanQRBtn.setOnClickListener(view -> {
|
||||
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
|
||||
intentIntegrator.setPrompt("Go to textbee.dev/dashboard and click Register Device to generate QR Code");
|
||||
intentIntegrator.setRequestCode(SCAN_QR_REQUEST_CODE);
|
||||
intentIntegrator.initiateScan();
|
||||
});
|
||||
}
|
||||
|
||||
private void renderAvailableSimOptions() {
|
||||
try {
|
||||
defaultSimSlotRadioGroup.removeAllViews();
|
||||
RadioButton defaultSimSlotRadioBtn = new RadioButton(mContext);
|
||||
defaultSimSlotRadioBtn.setText("Device Default");
|
||||
defaultSimSlotRadioBtn.setId((int)123456);
|
||||
defaultSimSlotRadioGroup.addView(defaultSimSlotRadioBtn);
|
||||
TextBeeUtils.getAvailableSimSlots(mContext).forEach(subscriptionInfo -> {
|
||||
String simInfo = "SIM " + (subscriptionInfo.getSimSlotIndex() + 1) + " (" + subscriptionInfo.getDisplayName() + ")";
|
||||
RadioButton radioButton = new RadioButton(mContext);
|
||||
radioButton.setText(simInfo);
|
||||
radioButton.setId(subscriptionInfo.getSubscriptionId());
|
||||
defaultSimSlotRadioGroup.addView(radioButton);
|
||||
});
|
||||
|
||||
int preferredSim = SharedPreferenceHelper.getSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1);
|
||||
if (preferredSim == -1) {
|
||||
defaultSimSlotRadioGroup.check(defaultSimSlotRadioBtn.getId());
|
||||
} else {
|
||||
defaultSimSlotRadioGroup.check(preferredSim);
|
||||
}
|
||||
defaultSimSlotRadioGroup.setOnCheckedChangeListener((radioGroup, i) -> {
|
||||
RadioButton radioButton = findViewById(i);
|
||||
if (radioButton == null) {
|
||||
return;
|
||||
}
|
||||
radioButton.setChecked(true);
|
||||
if("Device Default".equals(radioButton.getText().toString())) {
|
||||
SharedPreferenceHelper.clearSharedPreference(mContext, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY);
|
||||
} else {
|
||||
SharedPreferenceHelper.setSharedPreferenceInt(mContext, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, radioButton.getId());
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Snackbar.make(defaultSimSlotRadioGroup.getRootView(), "Error: " + e.getMessage(), Snackbar.LENGTH_LONG).show();
|
||||
Log.e(TAG, "SIM_SLOT_ERROR "+ e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
switch (requestCode) {
|
||||
case SEND_SMS_PERMISSION_REQUEST_CODE: {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(mContext, "Yay! Permission Granted.", Toast.LENGTH_LONG).show();
|
||||
} else {
|
||||
Toast.makeText(mContext, "Permission Denied :(", Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCode != PERMISSION_REQUEST_CODE) {
|
||||
return;
|
||||
}
|
||||
boolean allPermissionsGranted = Arrays.stream(permissions).allMatch(permission -> TextBeeUtils.isPermissionGranted(mContext, permission));
|
||||
if (allPermissionsGranted) {
|
||||
Snackbar.make(findViewById(R.id.grantSMSPermissionBtn), "All Permissions Granted", Snackbar.LENGTH_SHORT).show();
|
||||
grantSMSPermissionBtn.setEnabled(false);
|
||||
grantSMSPermissionBtn.setText("Permission Granted");
|
||||
renderAvailableSimOptions();
|
||||
} else {
|
||||
Snackbar.make(findViewById(R.id.grantSMSPermissionBtn), "Please Grant Required Permissions to continue", Snackbar.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRegisterDevice() {
|
||||
@@ -230,86 +241,63 @@ public class MainActivity extends AppCompatActivity {
|
||||
registerDeviceInput.setModel(Build.MODEL);
|
||||
registerDeviceInput.setBuildId(Build.ID);
|
||||
registerDeviceInput.setOs(Build.VERSION.BASE_OS);
|
||||
registerDeviceInput.setAppVersionCode(BuildConfig.VERSION_CODE);
|
||||
registerDeviceInput.setAppVersionName(BuildConfig.VERSION_NAME);
|
||||
|
||||
|
||||
Call<RegisterDeviceResponseDTO> apiCall = gatewayApiService.registerDevice(newKey, registerDeviceInput);
|
||||
Call<RegisterDeviceResponseDTO> apiCall = ApiManager.getApiService().registerDevice(newKey, registerDeviceInput);
|
||||
apiCall.enqueue(new Callback<RegisterDeviceResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<RegisterDeviceResponseDTO> call, Response<RegisterDeviceResponseDTO> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, "API_KEY", newKey);
|
||||
Log.e("API_RESP", response.toString());
|
||||
Snackbar.make(view, "Device Registration Successful :)", Snackbar.LENGTH_LONG).show();
|
||||
deviceId = response.body().data.get("_id").toString();
|
||||
deviceIdTxt.setText(deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, "DEVICE_ID", deviceId);
|
||||
|
||||
} else {
|
||||
Log.d(TAG, response.toString());
|
||||
if (!response.isSuccessful()) {
|
||||
Snackbar.make(view, response.message(), Snackbar.LENGTH_LONG).show();
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
return;
|
||||
}
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_API_KEY_KEY, newKey);
|
||||
Snackbar.make(view, "Device Registration Successful :)", Snackbar.LENGTH_LONG).show();
|
||||
deviceId = response.body().data.get("_id").toString();
|
||||
deviceIdTxt.setText(deviceId);
|
||||
SharedPreferenceHelper.setSharedPreferenceString(mContext, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, deviceId);
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
}
|
||||
|
||||
}
|
||||
@Override
|
||||
public void onFailure(Call<RegisterDeviceResponseDTO> call, Throwable t) {
|
||||
Snackbar.make(view, "An error occured :(", Snackbar.LENGTH_LONG).show();
|
||||
registerDeviceBtn.setEnabled(true);
|
||||
registerDeviceBtn.setText("Update");
|
||||
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void handleSMSRequestPermission(View view) {
|
||||
if (isSMSPermissionGranted(mContext) && isReadPhoneStatePermissionGranted(mContext)) {
|
||||
private void handleRequestPermissions(View view) {
|
||||
boolean allPermissionsGranted = Arrays.stream(AppConstants.requiredPermissions).allMatch(permission -> TextBeeUtils.isPermissionGranted(mContext, permission));
|
||||
if (allPermissionsGranted) {
|
||||
Snackbar.make(view, "Already got permissions", Snackbar.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Snackbar.make(view, "Grant SMS Permissions to continue", Snackbar.LENGTH_SHORT).show();
|
||||
ActivityCompat.requestPermissions(MainActivity.this,
|
||||
new String[]{Manifest.permission.SEND_SMS, Manifest.permission.READ_PHONE_STATE
|
||||
}, SEND_SMS_PERMISSION_REQUEST_CODE);
|
||||
|
||||
return;
|
||||
}
|
||||
String[] permissionsToRequest = Arrays.stream(AppConstants.requiredPermissions).filter(permission -> !TextBeeUtils.isPermissionGranted(mContext, permission)).toArray(String[]::new);
|
||||
Snackbar.make(view, "Please Grant Required Permissions to continue", Snackbar.LENGTH_SHORT).show();
|
||||
ActivityCompat.requestPermissions(this, permissionsToRequest, PERMISSION_REQUEST_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == SCAN_QR_REQUEST_CODE) {
|
||||
IntentResult intentResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (intentResult != null) {
|
||||
if (intentResult.getContents() == null) {
|
||||
Toast.makeText(getBaseContext(), "Canceled", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
String scannedQR = intentResult.getContents();
|
||||
apiKeyEditText.setText(scannedQR);
|
||||
handleRegisterDevice();
|
||||
}
|
||||
if (intentResult == null || intentResult.getContents() == null) {
|
||||
Toast.makeText(getBaseContext(), "Canceled", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
String scannedQR = intentResult.getContents();
|
||||
apiKeyEditText.setText(scannedQR);
|
||||
handleRegisterDevice();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isSMSPermissionGranted(Context context) {
|
||||
return ContextCompat.checkSelfPermission(context, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private boolean isReadPhoneStatePermissionGranted(Context context) {
|
||||
return ContextCompat.checkSelfPermission(context, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
private List<SubscriptionInfo> getAvailableSimSlots() {
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
SubscriptionManager subscriptionManager = SubscriptionManager.from(mContext);
|
||||
return subscriptionManager.getActiveSubscriptionInfoList();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//package com.vernu.sms.database.local;
|
||||
//
|
||||
//import android.content.Context;
|
||||
//import androidx.room.Database;
|
||||
//import androidx.room.Room;
|
||||
//import androidx.room.RoomDatabase;
|
||||
//
|
||||
//@Database(entities = {SMS.class}, version = 2)
|
||||
//public abstract class AppDatabase extends RoomDatabase {
|
||||
// private static volatile AppDatabase INSTANCE;
|
||||
//
|
||||
// public static AppDatabase getInstance(Context context) {
|
||||
// if (INSTANCE == null) {
|
||||
// synchronized (AppDatabase.class) {
|
||||
// if (INSTANCE == null) {
|
||||
// INSTANCE = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "db1")
|
||||
// .build();
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// return INSTANCE;
|
||||
// }
|
||||
//
|
||||
// public abstract SMSDao localReceivedSMSDao();
|
||||
//}
|
||||
@@ -0,0 +1,17 @@
|
||||
//package com.vernu.sms.database.local;
|
||||
//
|
||||
//import androidx.room.TypeConverter;
|
||||
//
|
||||
//import java.util.Date;
|
||||
//
|
||||
//public class DateConverter {
|
||||
// @TypeConverter
|
||||
// public static Date toDate(Long dateLong) {
|
||||
// return dateLong == null ? null : new Date(dateLong);
|
||||
// }
|
||||
//
|
||||
// @TypeConverter
|
||||
// public static Long fromDate(Date date) {
|
||||
// return date == null ? null : date.getTime();
|
||||
// }
|
||||
//}
|
||||
193
android/app/src/main/java/com/vernu/sms/database/local/SMS.java
Normal file
193
android/app/src/main/java/com/vernu/sms/database/local/SMS.java
Normal file
@@ -0,0 +1,193 @@
|
||||
//package com.vernu.sms.database.local;
|
||||
//
|
||||
//import androidx.annotation.NonNull;
|
||||
//import androidx.room.ColumnInfo;
|
||||
//import androidx.room.Entity;
|
||||
//import androidx.room.PrimaryKey;
|
||||
//import androidx.room.TypeConverters;
|
||||
//
|
||||
//import java.util.Date;
|
||||
//
|
||||
//@Entity(tableName = "sms")
|
||||
//@TypeConverters(DateConverter.class)
|
||||
//public class SMS {
|
||||
//
|
||||
// public SMS() {
|
||||
// type = null;
|
||||
// }
|
||||
//
|
||||
// @PrimaryKey(autoGenerate = true)
|
||||
// private int id;
|
||||
//
|
||||
// // This is the ID of the SMS in the server
|
||||
// @ColumnInfo(name = "_id")
|
||||
// private String _id;
|
||||
//
|
||||
// @ColumnInfo(name = "message")
|
||||
// private String message = "";
|
||||
//
|
||||
// @ColumnInfo(name = "encrypted_message")
|
||||
// private String encryptedMessage = "";
|
||||
//
|
||||
// @ColumnInfo(name = "is_encrypted", defaultValue = "0")
|
||||
// private boolean isEncrypted = false;
|
||||
//
|
||||
// @ColumnInfo(name = "sender")
|
||||
// private String sender;
|
||||
//
|
||||
// @ColumnInfo(name = "recipient")
|
||||
// private String recipient;
|
||||
//
|
||||
// @ColumnInfo(name = "requested_at")
|
||||
// private Date requestedAt;
|
||||
//
|
||||
// @ColumnInfo(name = "sent_at")
|
||||
// private Date sentAt;
|
||||
//
|
||||
// @ColumnInfo(name = "delivered_at")
|
||||
// private Date deliveredAt;
|
||||
//
|
||||
// @ColumnInfo(name = "received_at")
|
||||
// private Date receivedAt;
|
||||
//
|
||||
// @NonNull
|
||||
// @ColumnInfo(name = "type")
|
||||
// private String type;
|
||||
//
|
||||
// @ColumnInfo(name = "server_acknowledged_at")
|
||||
// private Date serverAcknowledgedAt;
|
||||
//
|
||||
// public boolean hasServerAcknowledged() {
|
||||
// return serverAcknowledgedAt != null;
|
||||
// }
|
||||
//
|
||||
// @ColumnInfo(name = "last_acknowledged_request_at")
|
||||
// private Date lastAcknowledgedRequestAt;
|
||||
//
|
||||
// @ColumnInfo(name = "retry_count", defaultValue = "0")
|
||||
// private int retryCount = 0;
|
||||
//
|
||||
// public int getId() {
|
||||
// return id;
|
||||
// }
|
||||
//
|
||||
// public void setId(int id) {
|
||||
// this.id = id;
|
||||
// }
|
||||
//
|
||||
// public String get_id() {
|
||||
// return _id;
|
||||
// }
|
||||
//
|
||||
// public void set_id(String _id) {
|
||||
// this._id = _id;
|
||||
// }
|
||||
//
|
||||
// public String getMessage() {
|
||||
// return message;
|
||||
// }
|
||||
//
|
||||
// public void setMessage(String message) {
|
||||
// this.message = message;
|
||||
// }
|
||||
//
|
||||
// public String getEncryptedMessage() {
|
||||
// return encryptedMessage;
|
||||
// }
|
||||
//
|
||||
// public void setEncryptedMessage(String encryptedMessage) {
|
||||
// this.encryptedMessage = encryptedMessage;
|
||||
// }
|
||||
//
|
||||
// public boolean getIsEncrypted() {
|
||||
// return isEncrypted;
|
||||
// }
|
||||
//
|
||||
// public void setIsEncrypted(boolean isEncrypted) {
|
||||
// this.isEncrypted = isEncrypted;
|
||||
// }
|
||||
//
|
||||
// public String getSender() {
|
||||
// return sender;
|
||||
// }
|
||||
//
|
||||
// public void setSender(String sender) {
|
||||
// this.sender = sender;
|
||||
// }
|
||||
//
|
||||
// public String getRecipient() {
|
||||
// return recipient;
|
||||
// }
|
||||
//
|
||||
// public void setRecipient(String recipient) {
|
||||
// this.recipient = recipient;
|
||||
// }
|
||||
//
|
||||
// public Date getServerAcknowledgedAt() {
|
||||
// return serverAcknowledgedAt;
|
||||
// }
|
||||
//
|
||||
// public void setServerAcknowledgedAt(Date serverAcknowledgedAt) {
|
||||
// this.serverAcknowledgedAt = serverAcknowledgedAt;
|
||||
// }
|
||||
//
|
||||
//
|
||||
//
|
||||
// public Date getRequestedAt() {
|
||||
// return requestedAt;
|
||||
// }
|
||||
//
|
||||
// public void setRequestedAt(Date requestedAt) {
|
||||
// this.requestedAt = requestedAt;
|
||||
// }
|
||||
//
|
||||
// public Date getSentAt() {
|
||||
// return sentAt;
|
||||
// }
|
||||
//
|
||||
// public void setSentAt(Date sentAt) {
|
||||
// this.sentAt = sentAt;
|
||||
// }
|
||||
//
|
||||
// public Date getDeliveredAt() {
|
||||
// return deliveredAt;
|
||||
// }
|
||||
//
|
||||
// public void setDeliveredAt(Date deliveredAt) {
|
||||
// this.deliveredAt = deliveredAt;
|
||||
// }
|
||||
//
|
||||
// public Date getReceivedAt() {
|
||||
// return receivedAt;
|
||||
// }
|
||||
//
|
||||
// public void setReceivedAt(Date receivedAt) {
|
||||
// this.receivedAt = receivedAt;
|
||||
// }
|
||||
//
|
||||
// @NonNull
|
||||
// public String getType() {
|
||||
// return type;
|
||||
// }
|
||||
//
|
||||
// public void setType(@NonNull String type) {
|
||||
// this.type = type;
|
||||
// }
|
||||
//
|
||||
//
|
||||
// public Date getLastAcknowledgedRequestAt() {
|
||||
// return lastAcknowledgedRequestAt;
|
||||
// }
|
||||
//
|
||||
// public void setLastAcknowledgedRequestAt(Date lastAcknowledgedRequestAt) {
|
||||
// this.lastAcknowledgedRequestAt = lastAcknowledgedRequestAt;
|
||||
// }
|
||||
//
|
||||
// public int getRetryCount() {
|
||||
// return retryCount;
|
||||
// }
|
||||
//
|
||||
// public void setRetryCount(int retryCount) {
|
||||
// this.retryCount = retryCount;
|
||||
// }
|
||||
//}
|
||||
@@ -0,0 +1,27 @@
|
||||
//package com.vernu.sms.database.local;
|
||||
//
|
||||
//import androidx.room.Dao;
|
||||
//import androidx.room.Delete;
|
||||
//import androidx.room.Insert;
|
||||
//import androidx.room.OnConflictStrategy;
|
||||
//import androidx.room.Query;
|
||||
//
|
||||
//import java.util.List;
|
||||
//
|
||||
//@Dao
|
||||
//public interface SMSDao {
|
||||
//
|
||||
// @Query("SELECT * FROM sms")
|
||||
// List<SMS> getAll();
|
||||
//
|
||||
// @Query("SELECT * FROM sms WHERE id IN (:smsIds)")
|
||||
// List<SMS> loadAllByIds(int[] smsIds);
|
||||
//
|
||||
// @Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
// void insertAll(SMS... sms);
|
||||
//
|
||||
//
|
||||
// @Delete
|
||||
// void delete(SMS sms);
|
||||
//
|
||||
//}
|
||||
@@ -11,7 +11,7 @@ public class RegisterDeviceInputDTO {
|
||||
private String os;
|
||||
private String osVersion;
|
||||
private String appVersionName;
|
||||
private String appVersionCode;
|
||||
private int appVersionCode;
|
||||
|
||||
public RegisterDeviceInputDTO() {
|
||||
}
|
||||
@@ -100,11 +100,11 @@ public class RegisterDeviceInputDTO {
|
||||
this.appVersionName = appVersionName;
|
||||
}
|
||||
|
||||
public String getAppVersionCode() {
|
||||
public int getAppVersionCode() {
|
||||
return appVersionCode;
|
||||
}
|
||||
|
||||
public void setAppVersionCode(String appVersionCode) {
|
||||
public void setAppVersionCode(int appVersionCode) {
|
||||
this.appVersionCode = appVersionCode;
|
||||
}
|
||||
}
|
||||
|
||||
42
android/app/src/main/java/com/vernu/sms/dtos/SMSDTO.java
Normal file
42
android/app/src/main/java/com/vernu/sms/dtos/SMSDTO.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package com.vernu.sms.dtos;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class SMSDTO {
|
||||
private String sender;
|
||||
private String message = "";
|
||||
private Date receivedAt;
|
||||
|
||||
public SMSDTO() {
|
||||
}
|
||||
|
||||
public SMSDTO(String sender, String message, Date receivedAt) {
|
||||
this.sender = sender;
|
||||
this.message = message;
|
||||
this.receivedAt = receivedAt;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return sender;
|
||||
}
|
||||
|
||||
public void setSender(String sender) {
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Date getReceivedAt() {
|
||||
return receivedAt;
|
||||
}
|
||||
|
||||
public void setReceivedAt(Date receivedAt) {
|
||||
this.receivedAt = receivedAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.vernu.sms.dtos;
|
||||
|
||||
public class SMSForwardResponseDTO {
|
||||
|
||||
public SMSForwardResponseDTO() {
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -44,4 +44,11 @@ public class SharedPreferenceHelper {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREF_FILE, 0);
|
||||
return settings.getBoolean(key, defValue);
|
||||
}
|
||||
|
||||
public static void clearSharedPreference(Context context, String key) {
|
||||
SharedPreferences settings = context.getSharedPreferences(PREF_FILE, 0);
|
||||
SharedPreferences.Editor editor = settings.edit();
|
||||
editor.remove(key);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
package com.vernu.sms.models;
|
||||
|
||||
public class SMSPayload {
|
||||
|
||||
private String[] recipients;
|
||||
private String message;
|
||||
|
||||
// Legacy fields that are no longer used
|
||||
private String[] receivers;
|
||||
private String smsBody;
|
||||
|
||||
public SMSPayload(String[] receivers, String smsBody) {
|
||||
this.receivers = receivers;
|
||||
this.smsBody = smsBody;
|
||||
public SMSPayload() {
|
||||
}
|
||||
|
||||
public String[] getReceivers() {
|
||||
return receivers;
|
||||
public String[] getRecipients() {
|
||||
return recipients;
|
||||
}
|
||||
|
||||
public void setReceivers(String[] receivers) {
|
||||
this.receivers = receivers;
|
||||
public void setRecipients(String[] recipients) {
|
||||
this.recipients = recipients;
|
||||
}
|
||||
|
||||
public String getSmsBody() {
|
||||
return smsBody;
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setSmsBody(String smsBody) {
|
||||
this.smsBody = smsBody;
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.vernu.sms.receivers;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import com.vernu.sms.TextBeeUtils;
|
||||
import com.vernu.sms.services.StickyNotificationService;
|
||||
|
||||
public class BootCompletedReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
// if(TextBeeUtils.isPermissionGranted(context, Manifest.permission.RECEIVE_SMS)){
|
||||
// TextBeeUtils.startStickyNotificationService(context);
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.vernu.sms.receivers;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.provider.Telephony;
|
||||
import android.telephony.SmsMessage;
|
||||
import android.util.Log;
|
||||
import com.vernu.sms.ApiManager;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.dtos.SMSForwardResponseDTO;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class SMSBroadcastReceiver extends BroadcastReceiver {
|
||||
private static final String TAG = "SMSBroadcastReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "onReceive: " + intent.getAction());
|
||||
|
||||
if (!Objects.equals(intent.getAction(), Telephony.Sms.Intents.SMS_RECEIVED_ACTION)) {
|
||||
Log.d(TAG, "Not Valid intent");
|
||||
return;
|
||||
}
|
||||
|
||||
SmsMessage[] messages = Telephony.Sms.Intents.getMessagesFromIntent(intent);
|
||||
if (messages == null) {
|
||||
Log.d(TAG, "No messages found");
|
||||
return;
|
||||
}
|
||||
|
||||
String deviceId = SharedPreferenceHelper.getSharedPreferenceString(context, AppConstants.SHARED_PREFS_DEVICE_ID_KEY, "");
|
||||
String apiKey = SharedPreferenceHelper.getSharedPreferenceString(context, AppConstants.SHARED_PREFS_API_KEY_KEY, "");
|
||||
boolean receiveSMSEnabled = SharedPreferenceHelper.getSharedPreferenceBoolean(context, AppConstants.SHARED_PREFS_RECEIVE_SMS_ENABLED_KEY, false);
|
||||
|
||||
if (deviceId.isEmpty() || apiKey.isEmpty() || !receiveSMSEnabled) {
|
||||
Log.d(TAG, "Device ID or API Key is empty or Receive SMS Feature is disabled");
|
||||
return;
|
||||
}
|
||||
|
||||
// SMS receivedSMS = new SMS();
|
||||
// receivedSMS.setType("RECEIVED");
|
||||
// for (SmsMessage message : messages) {
|
||||
// receivedSMS.setMessage(receivedSMS.getMessage() + message.getMessageBody());
|
||||
// receivedSMS.setSender(message.getOriginatingAddress());
|
||||
// receivedSMS.setReceivedAt(new Date(message.getTimestampMillis()));
|
||||
// }
|
||||
|
||||
SMSDTO receivedSMSDTO = new SMSDTO();
|
||||
|
||||
for (SmsMessage message : messages) {
|
||||
receivedSMSDTO.setMessage(receivedSMSDTO.getMessage() + message.getMessageBody());
|
||||
receivedSMSDTO.setSender(message.getOriginatingAddress());
|
||||
receivedSMSDTO.setReceivedAt(new Date(message.getTimestampMillis()));
|
||||
}
|
||||
// receivedSMSDTO.setSender(receivedSMS.getSender());
|
||||
// receivedSMSDTO.setMessage(receivedSMS.getMessage());
|
||||
// receivedSMSDTO.setReceivedAt(receivedSMS.getReceivedAt());
|
||||
|
||||
Call<SMSForwardResponseDTO> apiCall = ApiManager.getApiService().sendReceivedSMS(deviceId, apiKey, receivedSMSDTO);
|
||||
apiCall.enqueue(new retrofit2.Callback<SMSForwardResponseDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<SMSForwardResponseDTO> call, Response<SMSForwardResponseDTO> response) {
|
||||
// Date now = new Date();
|
||||
if (response.isSuccessful()) {
|
||||
Log.d(TAG, "SMS sent to server successfully");
|
||||
// receivedSMS.setLastAcknowledgedRequestAt(now);
|
||||
// receivedSMS.setServerAcknowledgedAt(now);
|
||||
// updateLocalReceivedSMS(receivedSMS, context);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to send SMS to server");
|
||||
// receivedSMS.setServerAcknowledgedAt(null);
|
||||
// receivedSMS.setLastAcknowledgedRequestAt(now);
|
||||
// receivedSMS.setRetryCount(localReceivedSMS.getRetryCount() + 1);
|
||||
// updateLocalReceivedSMS(receivedSMS, context);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(Call<SMSForwardResponseDTO> call, Throwable t) {
|
||||
Log.e(TAG, "Failed to send SMS to server", t);
|
||||
// receivedSMS.setServerAcknowledgedAt(null);
|
||||
// receivedSMS.setLastAcknowledgedRequestAt(new Date());
|
||||
// updateLocalReceivedSMS(receivedSMS, context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// private void updateLocalReceivedSMS(SMS localReceivedSMS, Context context) {
|
||||
// Executors.newSingleThreadExecutor().execute(() -> {
|
||||
// AppDatabase appDatabase = AppDatabase.getInstance(context);
|
||||
// appDatabase.localReceivedSMSDao().insertAll(localReceivedSMS);
|
||||
// });
|
||||
// }
|
||||
}
|
||||
@@ -9,42 +9,40 @@ import android.media.RingtoneManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
import com.google.gson.Gson;
|
||||
import com.vernu.sms.AppConstants;
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.activities.MainActivity;
|
||||
import com.vernu.sms.helpers.SMSHelper;
|
||||
import com.vernu.sms.helpers.SharedPreferenceHelper;
|
||||
import com.vernu.sms.models.SMSPayload;
|
||||
|
||||
|
||||
public class FCMService extends FirebaseMessagingService {
|
||||
|
||||
private static final String TAG = "MyFirebaseMsgService";
|
||||
private static final String TAG = "FirebaseMessagingService";
|
||||
private static final String DEFAULT_NOTIFICATION_CHANNEL_ID = "N1";
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
|
||||
Log.d("FCM_MESSAGE", remoteMessage.getData().toString());
|
||||
Log.d(TAG, remoteMessage.getData().toString());
|
||||
|
||||
Gson gson = new Gson();
|
||||
SMSPayload smsPayload = gson.fromJson(remoteMessage.getData().get("smsData"), SMSPayload.class);
|
||||
|
||||
// Check if message contains a data payload.
|
||||
if (remoteMessage.getData().size() > 0) {
|
||||
int preferedSim = SharedPreferenceHelper.getSharedPreferenceInt(this, "PREFERED_SIM", -1);
|
||||
for (String receiver : smsPayload.getReceivers()) {
|
||||
int preferedSim = SharedPreferenceHelper.getSharedPreferenceInt(this, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1);
|
||||
for (String receiver : smsPayload.getRecipients()) {
|
||||
if(preferedSim == -1) {
|
||||
SMSHelper.sendSMS(receiver, smsPayload.getSmsBody());
|
||||
SMSHelper.sendSMS(receiver, smsPayload.getMessage());
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
SMSHelper.sendSMSFromSpecificSim(receiver, smsPayload.getSmsBody(), preferedSim);
|
||||
SMSHelper.sendSMSFromSpecificSim(receiver, smsPayload.getMessage(), preferedSim);
|
||||
} catch(Exception e) {
|
||||
Log.d("SMS_SEND_ERROR", e.getMessage());
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package com.vernu.sms.services;
|
||||
|
||||
import com.vernu.sms.dtos.SMSDTO;
|
||||
import com.vernu.sms.dtos.SMSForwardResponseDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceInputDTO;
|
||||
import com.vernu.sms.dtos.RegisterDeviceResponseDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.PATCH;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
public interface GatewayApiService {
|
||||
@POST("gateway/devices")
|
||||
Call<RegisterDeviceResponseDTO> registerDevice(@Query("apiKey") String apiKey, @Body() RegisterDeviceInputDTO body);
|
||||
Call<RegisterDeviceResponseDTO> registerDevice(@Header("x-api-key") String apiKey, @Body() RegisterDeviceInputDTO body);
|
||||
|
||||
@PATCH("gateway/devices/{deviceId}")
|
||||
Call<RegisterDeviceResponseDTO> updateDevice(@Path("deviceId") String deviceId, @Query("apiKey") String apiKey, @Body() RegisterDeviceInputDTO body);
|
||||
Call<RegisterDeviceResponseDTO> updateDevice(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() RegisterDeviceInputDTO body);
|
||||
|
||||
@POST("gateway/devices/{deviceId}/receiveSMS")
|
||||
Call<SMSForwardResponseDTO> sendReceivedSMS(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() SMSDTO body);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.vernu.sms.services;
|
||||
|
||||
import android.app.*;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.IBinder;
|
||||
import android.provider.Telephony;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.vernu.sms.R;
|
||||
import com.vernu.sms.activities.MainActivity;
|
||||
import com.vernu.sms.receivers.SMSBroadcastReceiver;
|
||||
|
||||
public class StickyNotificationService extends Service {
|
||||
|
||||
private static final String TAG = "StickyNotificationService";
|
||||
private final BroadcastReceiver receiver = new SMSBroadcastReceiver();
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
Log.i(TAG, "Service onBind " + intent.getAction());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
Log.i(TAG, "Service Started");
|
||||
|
||||
|
||||
// IntentFilter filter = new IntentFilter();
|
||||
// filter.addAction(Telephony.Sms.Intents.SMS_RECEIVED_ACTION);
|
||||
// filter.addAction(android.telephony.TelephonyManager.ACTION_PHONE_STATE_CHANGED);
|
||||
// registerReceiver(receiver, filter);
|
||||
//
|
||||
// Notification notification = createNotification();
|
||||
// startForeground(1, notification);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Log.i(TAG, "Received start id " + startId + ": " + intent);
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// unregisterReceiver(receiver);
|
||||
Log.i(TAG, "StickyNotificationService destroyed");
|
||||
// Toast.makeText(this, "Service destroyed", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private Notification createNotification() {
|
||||
String notificationChannelId = "stickyNotificationChannel";
|
||||
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = null;
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
channel = new NotificationChannel(notificationChannelId, notificationChannelId, NotificationManager.IMPORTANCE_HIGH);
|
||||
channel.enableVibration(false);
|
||||
channel.setShowBadge(false);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
|
||||
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Notification.Builder builder = new Notification.Builder(this, notificationChannelId);
|
||||
return builder.setContentTitle("TextBee is running").setContentText("TextBee is running in the background.").setContentIntent(pendingIntent).setOngoing(true).setSmallIcon(R.drawable.ic_launcher_foreground).build();
|
||||
} else {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, notificationChannelId);
|
||||
return builder.setContentTitle("TextBee is running").setContentText("TextBee is running in the background.").setOngoing(true).setSmallIcon(R.drawable.ic_launcher_foreground).build();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,82 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.MainActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#ccccccee"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="How To Use"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Go to textbee.dev/dashboard and click register device, then copy and paste the api key generated or scan the QR code" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/grantSMSPermissionBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Grant SMS Permission"
|
||||
android:visibility="visible" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Default SIM"
|
||||
android:textStyle="bold" />
|
||||
<RadioGroup
|
||||
android:id="@+id/defaultSimSlotRadioGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
</RadioGroup>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/scrollView2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="606dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="1.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Enter your API key or scan the QR code below to get started"
|
||||
android:textSize="20dp"
|
||||
android:layout_margin="5dp"
|
||||
android:textAlignment="center"
|
||||
android:layout_gravity="center" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#ccccccee"
|
||||
android:layout_margin="5dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
@@ -91,7 +46,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ems="10"
|
||||
android:hint="key"
|
||||
android:hint="API Key"
|
||||
android:inputType="text"
|
||||
android:minHeight="48dp"
|
||||
android:textIsSelectable="true" />
|
||||
@@ -137,6 +92,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:layout_margin="5dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
@@ -229,9 +185,119 @@
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="30dp"
|
||||
android:orientation="vertical"
|
||||
android:padding="10px">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Configuration"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="2dp"
|
||||
android:background="#000000" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/grantSMSPermissionBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Grant Permissions"
|
||||
android:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="TextBee will only work if you grant SMS Permissions"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#000000" />
|
||||
|
||||
<Switch
|
||||
android:id="@+id/receiveSMSSwitch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="5dp"
|
||||
android:minHeight="32dp"
|
||||
android:text="Receive SMS" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Toggle this if you want to receive SMS"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="#000000" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Default SIM"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/defaultSimSlotRadioGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"></RadioGroup>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Select your preferred SIM for sending SMS"
|
||||
android:textSize="14dp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="#ccccccee"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="30dp"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="How To Use"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Go to textbee.dev/dashboard and click `generate API Key / Get started`, then copy and paste the API key generated or scan the QR code" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
@@ -1,6 +1,7 @@
|
||||
PORT=
|
||||
MONGO_URI=
|
||||
JWT_SECRET=secret
|
||||
JWT_EXPIRATION=60d
|
||||
|
||||
FIREBASE_PROJECT_ID=
|
||||
FIREBASE_PRIVATE_KEY_ID=
|
||||
|
||||
@@ -27,6 +27,7 @@ export class AuthController {
|
||||
constructor(private authService: AuthService) {}
|
||||
|
||||
@ApiOperation({ summary: 'Login' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/login')
|
||||
async login(@Body() input: LoginInputDTO) {
|
||||
const data = await this.authService.login(input)
|
||||
@@ -34,6 +35,7 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Login With Google' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/google-login')
|
||||
async googleLogin(@Body() input: any) {
|
||||
const data = await this.authService.loginWithGoogle(input.idToken)
|
||||
@@ -48,11 +50,6 @@ export class AuthController {
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Get current logged in user' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(AuthGuard)
|
||||
@Get('/who-am-i')
|
||||
@@ -62,11 +59,6 @@ export class AuthController {
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Generate Api Key' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@Post('/api-keys')
|
||||
async generateApiKey(@Request() req) {
|
||||
@@ -76,11 +68,6 @@ export class AuthController {
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Get Api Key List (masked***)' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@ApiBearerAuth()
|
||||
@Get('/api-keys')
|
||||
async getApiKey(@Request() req) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
PasswordReset,
|
||||
PasswordResetSchema,
|
||||
} from './schemas/password-reset.schema'
|
||||
import { AccessLog, AccessLogSchema } from './schemas/access-log.schema'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -24,12 +25,16 @@ import {
|
||||
name: PasswordReset.name,
|
||||
schema: PasswordResetSchema,
|
||||
},
|
||||
{
|
||||
name: AccessLog.name,
|
||||
schema: AccessLogSchema,
|
||||
},
|
||||
]),
|
||||
UsersModule,
|
||||
PassportModule,
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: { expiresIn: '180d' },
|
||||
signOptions: { expiresIn: process.env.JWT_EXPIRATION || '60d' },
|
||||
}),
|
||||
MailModule,
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './schemas/password-reset.schema'
|
||||
import { MailService } from 'src/mail/mail.service'
|
||||
import { RequestResetPasswordInputDTO, ResetPasswordInputDTO } from './auth.dto'
|
||||
import { AccessLog } from './schemas/access-log.schema'
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@@ -22,6 +23,7 @@ export class AuthService {
|
||||
@InjectModel(ApiKey.name) private apiKeyModel: Model<ApiKeyDocument>,
|
||||
@InjectModel(PasswordReset.name)
|
||||
private passwordResetModel: Model<PasswordResetDocument>,
|
||||
@InjectModel(AccessLog.name) private accessLogModel: Model<AccessLog>,
|
||||
private readonly mailService: MailService,
|
||||
) {}
|
||||
|
||||
@@ -194,6 +196,39 @@ export class AuthService {
|
||||
)
|
||||
}
|
||||
|
||||
await this.apiKeyModel.deleteOne({ _id: apiKeyId })
|
||||
// await this.apiKeyModel.deleteOne({ _id: apiKeyId })
|
||||
}
|
||||
|
||||
async trackAccessLog({ request }) {
|
||||
const { apiKey, user, method, url, ip, headers } = request
|
||||
const userAgent = headers['user-agent']
|
||||
|
||||
if (request.apiKey) {
|
||||
this.apiKeyModel
|
||||
.findByIdAndUpdate(
|
||||
apiKey._id,
|
||||
{ $inc: { usageCount: 1 }, lastUsedAt: new Date() },
|
||||
{ new: true },
|
||||
)
|
||||
.exec()
|
||||
.catch((e) => {
|
||||
console.log('Failed to update api key usage count')
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
|
||||
this.accessLogModel
|
||||
.create({
|
||||
apiKey,
|
||||
user,
|
||||
method,
|
||||
url: url.split('?')[0],
|
||||
ip,
|
||||
userAgent,
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log('Failed to track access log')
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,25 +20,29 @@ export class AuthGuard implements CanActivate {
|
||||
) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
var userId
|
||||
|
||||
const request = context.switchToHttp().getRequest()
|
||||
let userId
|
||||
const apiKeyString = request.headers['x-api-key'] || request.query.apiKey
|
||||
if (request.headers.authorization?.startsWith('Bearer ')) {
|
||||
const bearerToken = request.headers.authorization.split(' ')[1]
|
||||
const payload = this.jwtService.verify(bearerToken)
|
||||
userId = payload.sub
|
||||
}
|
||||
|
||||
// check apiKey in query params
|
||||
else if (request.query.apiKey) {
|
||||
const apiKeyStr = request.query.apiKey
|
||||
const regex = new RegExp(`^${apiKeyStr.substr(0, 17)}`, 'g')
|
||||
try {
|
||||
const payload = this.jwtService.verify(bearerToken)
|
||||
userId = payload.sub
|
||||
} catch (e) {
|
||||
throw new HttpException(
|
||||
{ error: 'Unauthorized' },
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
)
|
||||
}
|
||||
} else if (apiKeyString) {
|
||||
const regex = new RegExp(`^${apiKeyString.substr(0, 17)}`, 'g')
|
||||
const apiKey = await this.authService.findApiKey({
|
||||
apiKey: { $regex: regex },
|
||||
})
|
||||
|
||||
if (apiKey && bcrypt.compareSync(apiKeyStr, apiKey.hashedApiKey)) {
|
||||
if (apiKey && bcrypt.compareSync(apiKeyString, apiKey.hashedApiKey)) {
|
||||
userId = apiKey.user
|
||||
request.apiKey = apiKey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +50,7 @@ export class AuthGuard implements CanActivate {
|
||||
const user = await this.usersService.findOne({ _id: userId })
|
||||
if (user) {
|
||||
request.user = user
|
||||
this.authService.trackAccessLog({ request })
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
31
api/src/auth/schemas/access-log.schema.ts
Normal file
31
api/src/auth/schemas/access-log.schema.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
|
||||
import { Document, Types } from 'mongoose'
|
||||
import { User } from '../../users/schemas/user.schema'
|
||||
import { ApiKey } from './api-key.schema'
|
||||
|
||||
export type AccessLogDocument = AccessLog & Document
|
||||
|
||||
@Schema({ timestamps: true })
|
||||
export class AccessLog {
|
||||
_id?: Types.ObjectId
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: ApiKey.name })
|
||||
apiKey: ApiKey
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: User.name })
|
||||
user: User
|
||||
|
||||
@Prop({ type: String })
|
||||
url: string
|
||||
|
||||
@Prop({ type: String })
|
||||
method: string
|
||||
|
||||
@Prop({ type: String })
|
||||
ip: string
|
||||
|
||||
@Prop({ type: String })
|
||||
userAgent: string
|
||||
}
|
||||
|
||||
export const AccessLogSchema = SchemaFactory.createForClass(AccessLog)
|
||||
@@ -16,6 +16,12 @@ export class ApiKey {
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: User.name })
|
||||
user: User
|
||||
|
||||
@Prop({ type: Number, default: 0 })
|
||||
usageCount: number
|
||||
|
||||
@Prop({ type: Date })
|
||||
lastUsedAt: Date
|
||||
}
|
||||
|
||||
export const ApiKeySchema = SchemaFactory.createForClass(ApiKey)
|
||||
|
||||
@@ -8,10 +8,23 @@ import {
|
||||
Request,
|
||||
Get,
|
||||
Delete,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
} from '@nestjs/common'
|
||||
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiQuery,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger'
|
||||
import { AuthGuard } from '../auth/guards/auth.guard'
|
||||
import { RegisterDeviceInputDTO, SendSMSInputDTO } from './gateway.dto'
|
||||
import {
|
||||
ReceivedSMSDTO,
|
||||
RegisterDeviceInputDTO,
|
||||
RetrieveSMSResponseDTO,
|
||||
SendSMSInputDTO,
|
||||
} from './gateway.dto'
|
||||
import { GatewayService } from './gateway.service'
|
||||
import { CanModifyDevice } from './guards/can-modify-device.guard'
|
||||
|
||||
@@ -30,11 +43,6 @@ export class GatewayController {
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Register device' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@Post('/devices')
|
||||
async registerDevice(@Body() input: RegisterDeviceInputDTO, @Request() req) {
|
||||
const data = await this.gatewayService.registerDevice(input, req.user)
|
||||
@@ -43,11 +51,6 @@ export class GatewayController {
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'List of registered devices' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@Get('/devices')
|
||||
async getDevices(@Request() req) {
|
||||
const data = await this.gatewayService.getDevicesForUser(req.user)
|
||||
@@ -55,11 +58,6 @@ export class GatewayController {
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Update device' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Patch('/devices/:id')
|
||||
async updateDevice(
|
||||
@@ -71,11 +69,6 @@ export class GatewayController {
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Delete device' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Delete('/devices/:id')
|
||||
async deleteDevice(@Param('id') deviceId: string) {
|
||||
@@ -84,11 +77,6 @@ export class GatewayController {
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Send SMS to a device' })
|
||||
@ApiQuery({
|
||||
name: 'apiKey',
|
||||
required: false,
|
||||
description: 'Required if jwt bearer token not provided',
|
||||
})
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Post('/devices/:id/sendSMS')
|
||||
async sendSMS(
|
||||
@@ -98,4 +86,24 @@ export class GatewayController {
|
||||
const data = await this.gatewayService.sendSMS(deviceId, smsData)
|
||||
return { data }
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Received SMS from a device' })
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post('/devices/:id/receiveSMS')
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
async receiveSMS(@Param('id') deviceId: string, @Body() dto: ReceivedSMSDTO) {
|
||||
const data = await this.gatewayService.receiveSMS(deviceId, dto)
|
||||
return { data }
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Get received SMS from a device' })
|
||||
@ApiResponse({ status: 200, type: RetrieveSMSResponseDTO })
|
||||
@UseGuards(AuthGuard, CanModifyDevice)
|
||||
@Get('/devices/:id/getReceivedSMS')
|
||||
async getReceivedSMS(
|
||||
@Param('id') deviceId: string,
|
||||
): Promise<RetrieveSMSResponseDTO> {
|
||||
const data = await this.gatewayService.getReceivedSMS(deviceId)
|
||||
return { data }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,16 +39,144 @@ export class SMSData {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'SMS text',
|
||||
description: 'The message to send',
|
||||
})
|
||||
smsBody: string
|
||||
message: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Array,
|
||||
required: true,
|
||||
description: 'Array of phone numbers',
|
||||
description: 'List of phone numbers to send the SMS to',
|
||||
example: ['+2519xxxxxxxx', '+2517xxxxxxxx'],
|
||||
})
|
||||
recipients: string[]
|
||||
|
||||
// TODO: restructure the Payload such that it contains bactchId, smsId, recipients and message in an optimized way
|
||||
// message: string
|
||||
// bactchId: string
|
||||
// list: {
|
||||
// smsId: string
|
||||
// recipient: string
|
||||
// }
|
||||
|
||||
// Legacy fields to be removed in the future
|
||||
// @ApiProperty({
|
||||
// type: String,
|
||||
// required: true,
|
||||
// description: '(Legacy) Will be Replace with `message` field in the future',
|
||||
// })
|
||||
smsBody: string
|
||||
|
||||
// @ApiProperty({
|
||||
// type: Array,
|
||||
// required: false,
|
||||
// description:
|
||||
// '(Legacy) Will be Replace with `recipients` field in the future',
|
||||
// example: ['+2519xxxxxxxx', '+2517xxxxxxxx'],
|
||||
// })
|
||||
receivers: string[]
|
||||
}
|
||||
export class SendSMSInputDTO extends SMSData {}
|
||||
|
||||
export class ReceivedSMSDTO {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The message received',
|
||||
})
|
||||
message: string
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The phone number of the sender',
|
||||
})
|
||||
sender: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
required: true,
|
||||
description: 'The time the message was received',
|
||||
})
|
||||
receivedAt: Date
|
||||
}
|
||||
|
||||
export class DeviceDTO {
|
||||
@ApiProperty({ type: String })
|
||||
_id: string
|
||||
|
||||
@ApiProperty({ type: Boolean })
|
||||
enabled: boolean
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
brand: string
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
manufacturer: string
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
model: string
|
||||
|
||||
@ApiProperty({ type: String })
|
||||
buildId: string
|
||||
}
|
||||
|
||||
export class RetrieveSMSDTO {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The id of the received SMS',
|
||||
})
|
||||
_id: string
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The message received',
|
||||
})
|
||||
message: string
|
||||
|
||||
@ApiProperty({
|
||||
type: DeviceDTO,
|
||||
required: true,
|
||||
description: 'The device that received the message',
|
||||
})
|
||||
device: DeviceDTO
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
required: true,
|
||||
description: 'The phone number of the sender',
|
||||
})
|
||||
sender: string
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
required: true,
|
||||
description: 'The time the message was received',
|
||||
})
|
||||
receivedAt: Date
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
required: true,
|
||||
description: 'The time the message was created',
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@ApiProperty({
|
||||
type: Date,
|
||||
required: true,
|
||||
description: 'The time the message was last updated',
|
||||
})
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export class RetrieveSMSResponseDTO {
|
||||
@ApiProperty({
|
||||
type: [RetrieveSMSDTO],
|
||||
required: true,
|
||||
description: 'The received SMS data',
|
||||
})
|
||||
data: RetrieveSMSDTO[]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { GatewayController } from './gateway.controller'
|
||||
import { GatewayService } from './gateway.service'
|
||||
import { AuthModule } from '../auth/auth.module'
|
||||
import { UsersModule } from '../users/users.module'
|
||||
import { SMS, SMSSchema } from './schemas/sms.schema'
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -13,6 +14,10 @@ import { UsersModule } from '../users/users.module'
|
||||
name: Device.name,
|
||||
schema: DeviceSchema,
|
||||
},
|
||||
{
|
||||
name: SMS.name,
|
||||
schema: SMSSchema,
|
||||
},
|
||||
]),
|
||||
AuthModule,
|
||||
UsersModule,
|
||||
|
||||
@@ -3,13 +3,21 @@ import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Device, DeviceDocument } from './schemas/device.schema'
|
||||
import { Model } from 'mongoose'
|
||||
import * as firebaseAdmin from 'firebase-admin'
|
||||
import { RegisterDeviceInputDTO, SendSMSInputDTO } from './gateway.dto'
|
||||
import {
|
||||
ReceivedSMSDTO,
|
||||
RegisterDeviceInputDTO,
|
||||
RetrieveSMSDTO,
|
||||
SendSMSInputDTO,
|
||||
} from './gateway.dto'
|
||||
import { User } from '../users/schemas/user.schema'
|
||||
import { AuthService } from 'src/auth/auth.service'
|
||||
import { SMS } from './schemas/sms.schema'
|
||||
import { SMSType } from './sms-type.enum'
|
||||
@Injectable()
|
||||
export class GatewayService {
|
||||
constructor(
|
||||
@InjectModel(Device.name) private deviceModel: Model<DeviceDocument>,
|
||||
@InjectModel(SMS.name) private smsModel: Model<SMS>,
|
||||
private authService: AuthService,
|
||||
) {}
|
||||
|
||||
@@ -72,10 +80,19 @@ export class GatewayService {
|
||||
)
|
||||
}
|
||||
|
||||
return await this.deviceModel.findByIdAndDelete(deviceId)
|
||||
return {}
|
||||
// return await this.deviceModel.findByIdAndDelete(deviceId)
|
||||
}
|
||||
|
||||
async sendSMS(deviceId: string, smsData: SendSMSInputDTO): Promise<any> {
|
||||
const updatedSMSData = {
|
||||
message: smsData.message || smsData.smsBody,
|
||||
recipients: smsData.recipients || smsData.receivers,
|
||||
|
||||
// Legacy fields to be removed in the future
|
||||
smsBody: smsData.message || smsData.smsBody,
|
||||
receivers: smsData.recipients || smsData.receivers,
|
||||
}
|
||||
const device = await this.deviceModel.findById(deviceId)
|
||||
|
||||
if (!device?.enabled) {
|
||||
@@ -88,11 +105,15 @@ export class GatewayService {
|
||||
)
|
||||
}
|
||||
|
||||
const stringifiedSMSData = JSON.stringify(updatedSMSData)
|
||||
const payload: any = {
|
||||
data: {
|
||||
smsData: JSON.stringify(smsData),
|
||||
smsData: stringifiedSMSData,
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Save SMS and Implement a queue to send the SMS if recipients are too many
|
||||
|
||||
try {
|
||||
const response = await firebaseAdmin
|
||||
.messaging()
|
||||
@@ -100,7 +121,7 @@ export class GatewayService {
|
||||
|
||||
this.deviceModel
|
||||
.findByIdAndUpdate(deviceId, {
|
||||
$inc: { sentSMSCount: smsData.receivers.length },
|
||||
$inc: { sentSMSCount: updatedSMSData.recipients.length },
|
||||
})
|
||||
.exec()
|
||||
.catch((e) => {
|
||||
@@ -118,19 +139,98 @@ export class GatewayService {
|
||||
}
|
||||
}
|
||||
|
||||
async receiveSMS(deviceId: string, dto: ReceivedSMSDTO): Promise<any> {
|
||||
const device = await this.deviceModel.findById(deviceId)
|
||||
|
||||
if (!device) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Device does not exist',
|
||||
},
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
if (!dto.receivedAt || !dto.sender || !dto.message) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Invalid received SMS data',
|
||||
},
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
const sms = await this.smsModel.create({
|
||||
device: device._id,
|
||||
message: dto.message,
|
||||
type: SMSType.RECEIVED,
|
||||
sender: dto.sender,
|
||||
receivedAt: dto.receivedAt,
|
||||
})
|
||||
|
||||
this.deviceModel
|
||||
.findByIdAndUpdate(deviceId, {
|
||||
$inc: { receivedSMSCount: 1 },
|
||||
})
|
||||
.exec()
|
||||
.catch((e) => {
|
||||
console.log('Failed to update receivedSMSCount')
|
||||
console.log(e)
|
||||
})
|
||||
|
||||
// TODO: Implement webhook to forward received SMS to user's callback URL
|
||||
|
||||
return sms
|
||||
}
|
||||
|
||||
async getReceivedSMS(deviceId: string): Promise<RetrieveSMSDTO[]> {
|
||||
const device = await this.deviceModel.findById(deviceId)
|
||||
|
||||
if (!device) {
|
||||
throw new HttpException(
|
||||
{
|
||||
success: false,
|
||||
error: 'Device does not exist',
|
||||
},
|
||||
HttpStatus.BAD_REQUEST,
|
||||
)
|
||||
}
|
||||
|
||||
return await this.smsModel
|
||||
.find(
|
||||
{
|
||||
device: device._id,
|
||||
type: SMSType.RECEIVED,
|
||||
},
|
||||
null,
|
||||
{ sort: { receivedAt: -1 }, limit: 200 },
|
||||
)
|
||||
.populate({
|
||||
path: 'device',
|
||||
select: '_id brand model buildId enabled',
|
||||
})
|
||||
}
|
||||
|
||||
async getStatsForUser(user: User) {
|
||||
const devices = await this.deviceModel.find({ user: user._id })
|
||||
const apiKeys = await this.authService.getUserApiKeys(user)
|
||||
|
||||
const totalSMSCount = devices.reduce((acc, device) => {
|
||||
const totalSentSMSCount = devices.reduce((acc, device) => {
|
||||
return acc + (device.sentSMSCount || 0)
|
||||
}, 0)
|
||||
|
||||
const totalReceivedSMSCount = devices.reduce((acc, device) => {
|
||||
return acc + (device.receivedSMSCount || 0)
|
||||
}, 0)
|
||||
|
||||
const totalDeviceCount = devices.length
|
||||
const totalApiKeyCount = apiKeys.length
|
||||
|
||||
return {
|
||||
totalSMSCount,
|
||||
totalSentSMSCount,
|
||||
totalReceivedSMSCount,
|
||||
totalDeviceCount,
|
||||
totalApiKeyCount,
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ export class Device {
|
||||
|
||||
@Prop({ type: Number, default: 0 })
|
||||
sentSMSCount: number
|
||||
|
||||
@Prop({ type: Number, default: 0 })
|
||||
receivedSMSCount: number
|
||||
}
|
||||
|
||||
export const DeviceSchema = SchemaFactory.createForClass(Device)
|
||||
|
||||
@@ -8,14 +8,53 @@ export type SMSDocument = SMS & Document
|
||||
export class SMS {
|
||||
_id?: Types.ObjectId
|
||||
|
||||
@Prop({ type: Types.ObjectId, ref: Device.name })
|
||||
@Prop({ type: Types.ObjectId, ref: Device.name, required: true })
|
||||
device: Device
|
||||
|
||||
@Prop({ type: String, required: true })
|
||||
@Prop({ type: String })
|
||||
message: string
|
||||
|
||||
@Prop({ type: Boolean, default: false })
|
||||
encrypted: boolean
|
||||
|
||||
@Prop({ type: String })
|
||||
encryptedMessage: string
|
||||
|
||||
@Prop({ type: String, required: true })
|
||||
to: string
|
||||
type: string
|
||||
|
||||
// fields for incoming messages
|
||||
@Prop({ type: String })
|
||||
sender: string
|
||||
|
||||
@Prop({ type: Date })
|
||||
receivedAt: Date
|
||||
|
||||
// fields for outgoing messages
|
||||
@Prop({ type: String })
|
||||
recipient: string
|
||||
|
||||
@Prop({ type: Date })
|
||||
requestedAt: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
sentAt: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
deliveredAt: Date
|
||||
|
||||
@Prop({ type: Date })
|
||||
failedAt: Date
|
||||
|
||||
// @Prop({ type: String })
|
||||
// failureReason: string
|
||||
|
||||
// @Prop({ type: String })
|
||||
// status: string
|
||||
|
||||
// misc metadata for debugging
|
||||
@Prop({ type: Object })
|
||||
metadata: Record<string, any>
|
||||
}
|
||||
|
||||
export const SMSSchema = SchemaFactory.createForClass(SMS)
|
||||
|
||||
4
api/src/gateway/sms-type.enum.ts
Normal file
4
api/src/gateway/sms-type.enum.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export enum SMSType {
|
||||
SENT = 'SENT',
|
||||
RECEIVED = 'RECEIVED',
|
||||
}
|
||||
@@ -20,6 +20,11 @@ async function bootstrap() {
|
||||
.setDescription('TextBee - Android SMS Gateway API Docs')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.addApiKey({
|
||||
type: 'apiKey',
|
||||
name: 'x-api-key',
|
||||
in: 'header',
|
||||
})
|
||||
.build()
|
||||
const document = SwaggerModule.createDocument(app, config)
|
||||
SwaggerModule.setup('', app, document, {
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function Footer() {
|
||||
<Stack direction={'row'} spacing={6}>
|
||||
<Link href='/'>Home</Link>
|
||||
<Link href='/dashboard'>Dashboard</Link>
|
||||
<Link href='/android'>Download App</Link>
|
||||
<Link href='https://dl.textbee.dev' target='_blank'> Download App</Link>
|
||||
<Link href='https://github.com/vernu/textbee'>Github</Link>
|
||||
</Stack>
|
||||
</Container>
|
||||
|
||||
@@ -19,12 +19,30 @@ import Router from 'next/router'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { logout, selectAuthUser } from '../store/authSlice'
|
||||
import Image from 'next/image'
|
||||
import { useEffect } from 'react'
|
||||
import { authService } from '../services/authService'
|
||||
|
||||
export default function Navbar() {
|
||||
const dispatch = useDispatch()
|
||||
const { colorMode, toggleColorMode } = useColorMode()
|
||||
const authUser = useSelector(selectAuthUser)
|
||||
|
||||
useEffect(() => {
|
||||
const timout = setTimeout(async () => {
|
||||
if (authUser) {
|
||||
authService
|
||||
.whoAmI()
|
||||
.catch((e) => {
|
||||
if (e.response?.status === 401) {
|
||||
dispatch(logout())
|
||||
}
|
||||
})
|
||||
.then((res) => {})
|
||||
}
|
||||
}, 5000)
|
||||
return () => clearTimeout(timout)
|
||||
}, [authUser, dispatch])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
|
||||
29
web/components/dashboard/APIKeyAndDevices.tsx
Normal file
29
web/components/dashboard/APIKeyAndDevices.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { Box, SimpleGrid } from '@chakra-ui/react'
|
||||
|
||||
import React from 'react'
|
||||
import ErrorBoundary from '../ErrorBoundary'
|
||||
import ApiKeyList from './ApiKeyList'
|
||||
import DeviceList from './DeviceList'
|
||||
import GenerateApiKey from './GenerateApiKey'
|
||||
|
||||
export default function APIKeyAndDevices() {
|
||||
return (
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
|
||||
<Box maxW='xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
|
||||
<GenerateApiKey />
|
||||
</Box>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 5, lg: 8 }}>
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
|
||||
<ErrorBoundary>
|
||||
<ApiKeyList />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
|
||||
<ErrorBoundary>
|
||||
<DeviceList />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ const ApiKeyList = () => {
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table variant='simple'>
|
||||
<Table variant='striped'>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Your API Keys</Th>
|
||||
|
||||
@@ -76,7 +76,7 @@ const DeviceList = () => {
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table variant='simple'>
|
||||
<Table variant='striped'>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Your Devices</Th>
|
||||
|
||||
170
web/components/dashboard/ReceiveSMS.tsx
Normal file
170
web/components/dashboard/ReceiveSMS.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import {
|
||||
Alert,
|
||||
AlertIcon,
|
||||
Grid,
|
||||
GridItem,
|
||||
Spinner,
|
||||
Stack,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Table,
|
||||
TableContainer,
|
||||
Tabs,
|
||||
Tbody,
|
||||
Td,
|
||||
Th,
|
||||
Thead,
|
||||
Tr,
|
||||
} from '@chakra-ui/react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import {
|
||||
fetchReceivedSMSList,
|
||||
selectDeviceList,
|
||||
selectReceivedSMSList,
|
||||
} from '../../store/deviceSlice'
|
||||
import { useAppDispatch } from '../../store/hooks'
|
||||
import { selectAuthUser } from '../../store/authSlice'
|
||||
|
||||
export default function ReceiveSMS() {
|
||||
return (
|
||||
<>
|
||||
<Grid
|
||||
templateColumns={{ base: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' }}
|
||||
gap={6}
|
||||
>
|
||||
<GridItem colSpan={2}>
|
||||
<ReceivedSMSList />
|
||||
</GridItem>
|
||||
<GridItem colSpan={1}>
|
||||
<ReceiveSMSNotifications />
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ReceiveSMSNotifications = () => {
|
||||
return (
|
||||
<Stack spacing={3}>
|
||||
<Alert status='success'>
|
||||
<AlertIcon />
|
||||
You can now receive SMS and view them in the dashboard, or retreive them
|
||||
via the API
|
||||
</Alert>
|
||||
|
||||
<Alert status='warning'>
|
||||
<AlertIcon />
|
||||
To receive SMS, you need to have an active device that has receive SMS
|
||||
option enabled <small>(Turn on the switch in the app)</small>
|
||||
</Alert>
|
||||
|
||||
<Alert status='info'>
|
||||
<AlertIcon />
|
||||
Webhooks will be available soon 😉
|
||||
</Alert>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
const ReceivedSMSList = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
|
||||
const { loading: receivedSMSListLoading, data: receivedSMSListData } =
|
||||
useSelector(selectReceivedSMSList)
|
||||
const deviceList = useSelector(selectDeviceList)
|
||||
|
||||
const authUser = useSelector(selectAuthUser)
|
||||
|
||||
const activeDeviceId = useMemo(() => {
|
||||
return deviceList[tabIndex]?._id
|
||||
}, [tabIndex, deviceList])
|
||||
|
||||
useEffect(() => {
|
||||
if (authUser && activeDeviceId) {
|
||||
dispatch(fetchReceivedSMSList(activeDeviceId))
|
||||
}
|
||||
}, [dispatch, authUser, activeDeviceId])
|
||||
|
||||
if (!receivedSMSListLoading && (!deviceList || deviceList.length == 0)) {
|
||||
return (
|
||||
<Alert status='warning'>
|
||||
<AlertIcon />
|
||||
You dont have any devices yet. Please register a device to receive SMS
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tabs isLazy={false} index={tabIndex} onChange={setTabIndex}>
|
||||
<TabList>
|
||||
{deviceList.map(({ _id, brand, model }) => (
|
||||
<Tab key={_id}>{`${brand} ${model}`}</Tab>
|
||||
))}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
{deviceList.map(({ _id, brand, model }) => (
|
||||
<TabPanel key={_id}>
|
||||
<TableContainer>
|
||||
<Table variant='striped'>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>sender</Th>
|
||||
<Th colSpan={4}>message</Th>
|
||||
<Th>received at</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{receivedSMSListLoading && (
|
||||
<Tr>
|
||||
<Td colSpan={6} textAlign='center'>
|
||||
<Spinner size='lg' />
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
|
||||
{!receivedSMSListLoading &&
|
||||
receivedSMSListData.length == 0 && (
|
||||
<Td colSpan={6} textAlign='center'>
|
||||
No SMS received
|
||||
</Td>
|
||||
)}
|
||||
|
||||
{!receivedSMSListLoading &&
|
||||
receivedSMSListData.length > 0 &&
|
||||
receivedSMSListData.map(
|
||||
({ _id, sender, message, receivedAt }) => (
|
||||
<Tr key={_id}>
|
||||
<Td>{sender}</Td>
|
||||
<Td whiteSpace='pre-wrap' colSpan={4}>
|
||||
{message}
|
||||
</Td>
|
||||
<Td>
|
||||
{new Date(receivedAt).toLocaleString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
hour12: true,
|
||||
})}
|
||||
</Td>
|
||||
<Td></Td>
|
||||
</Tr>
|
||||
)
|
||||
)}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TabPanel>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Spinner,
|
||||
Textarea,
|
||||
useDisclosure,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState } from 'react'
|
||||
@@ -39,29 +31,33 @@ export const SendSMSForm = ({ deviceList, formData, handleChange }) => {
|
||||
value={formData.device}
|
||||
>
|
||||
{deviceList.map((device) => (
|
||||
<option key={device._id} value={device._id}>
|
||||
<option
|
||||
key={device._id}
|
||||
value={device._id}
|
||||
disabled={!device.enabled}
|
||||
>
|
||||
{device.model}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
<Box>
|
||||
<FormLabel htmlFor='receivers'>Receiver</FormLabel>
|
||||
<FormLabel htmlFor='recipient'>Recipient</FormLabel>
|
||||
<Input
|
||||
placeholder='receiver'
|
||||
name='receivers'
|
||||
placeholder='recipient'
|
||||
name='recipients'
|
||||
onChange={handleChange}
|
||||
value={formData.receivers}
|
||||
value={formData.recipients}
|
||||
type='tel'
|
||||
/>
|
||||
</Box>
|
||||
<Box>
|
||||
<FormLabel htmlFor='smsBody'>SMS Body</FormLabel>
|
||||
<FormLabel htmlFor='message'>Message</FormLabel>
|
||||
<Textarea
|
||||
id='smsBody'
|
||||
name='smsBody'
|
||||
id='message'
|
||||
name='message'
|
||||
onChange={handleChange}
|
||||
value={formData.smsBody}
|
||||
value={formData.message}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
@@ -69,7 +65,6 @@ export const SendSMSForm = ({ deviceList, formData, handleChange }) => {
|
||||
}
|
||||
|
||||
export default function SendSMS() {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure()
|
||||
const deviceList = useSelector(selectDeviceList)
|
||||
const toast = useToast()
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -78,16 +73,16 @@ export default function SendSMS() {
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
device: '',
|
||||
receivers: '',
|
||||
smsBody: '',
|
||||
recipients: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const handSend = (e) => {
|
||||
e.preventDefault()
|
||||
const { device: deviceId, receivers, smsBody } = formData
|
||||
const receiversArray = receivers.replace(' ', '').split(',')
|
||||
const { device: deviceId, recipients, message } = formData
|
||||
const recipientsArray = recipients.replace(' ', '').split(',')
|
||||
|
||||
if (!deviceId || !receivers || !smsBody) {
|
||||
if (!deviceId || !recipients || !message) {
|
||||
toast({
|
||||
title: 'Please fill all fields',
|
||||
status: 'error',
|
||||
@@ -95,17 +90,16 @@ export default function SendSMS() {
|
||||
return
|
||||
}
|
||||
|
||||
for (let receiver of receiversArray) {
|
||||
for (let recipient of recipientsArray) {
|
||||
// TODO: validate phone numbers
|
||||
}
|
||||
|
||||
|
||||
dispatch(
|
||||
sendSMS({
|
||||
deviceId,
|
||||
payload: {
|
||||
receivers: receiversArray,
|
||||
smsBody,
|
||||
recipients: recipientsArray,
|
||||
message,
|
||||
},
|
||||
})
|
||||
)
|
||||
@@ -120,39 +114,26 @@ export default function SendSMS() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justifyContent='flex-end' marginBottom={20}>
|
||||
<Button bg={'blue.400'} color={'white'} onClick={onOpen}>
|
||||
Send SMS
|
||||
</Button>
|
||||
</Flex>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 5, lg: 8 }}>
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
|
||||
<SendSMSForm
|
||||
deviceList={deviceList}
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
|
||||
<Modal isOpen={isOpen} onClose={onClose}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Send SMS</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<SendSMSForm
|
||||
deviceList={deviceList}
|
||||
formData={formData}
|
||||
handleChange={handleChange}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='ghost' mr={3} onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
colorScheme='blue'
|
||||
onClick={handSend}
|
||||
disabled={sendingSMS}
|
||||
>
|
||||
{sendingSMS ? <Spinner size='md' /> : 'Send'}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Button
|
||||
variant='outline'
|
||||
colorScheme='blue'
|
||||
onClick={handSend}
|
||||
disabled={sendingSMS}
|
||||
marginTop={3}
|
||||
>
|
||||
{sendingSMS ? <Spinner size='md' /> : 'Send'}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'></Box>
|
||||
</SimpleGrid>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Box, SimpleGrid, chakra } from '@chakra-ui/react'
|
||||
import { Box, Grid, GridItem, SimpleGrid, chakra } from '@chakra-ui/react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectAuthUser } from '../../store/authSlice'
|
||||
@@ -13,8 +13,12 @@ import { useAppDispatch, useAppSelector } from '../../store/hooks'
|
||||
const UserStats = () => {
|
||||
const authUser = useSelector(selectAuthUser)
|
||||
|
||||
const { totalApiKeyCount, totalDeviceCount, totalSMSCount } =
|
||||
useAppSelector(selectStatsData)
|
||||
const {
|
||||
totalApiKeyCount,
|
||||
totalDeviceCount,
|
||||
totalReceivedSMSCount,
|
||||
totalSentSMSCount,
|
||||
} = useAppSelector(selectStatsData)
|
||||
const statsLoading = useAppSelector(selectStatsLoading)
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -25,31 +29,47 @@ const UserStats = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }}>
|
||||
<chakra.h1
|
||||
textAlign={'center'}
|
||||
fontSize={'4xl'}
|
||||
py={10}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
Welcome {authUser?.name}
|
||||
</chakra.h1>
|
||||
<SimpleGrid columns={{ base: 3 }} spacing={{ base: 5, lg: 8 }}>
|
||||
<UserStatsCard
|
||||
title={'Registered '}
|
||||
stat={`${statsLoading ? '-:-' : totalDeviceCount} Devices`}
|
||||
/>
|
||||
<UserStatsCard
|
||||
title={'Generated'}
|
||||
stat={`${statsLoading ? '-:-' : totalApiKeyCount} API Keys`}
|
||||
/>
|
||||
<UserStatsCard
|
||||
title={'Sent'}
|
||||
stat={`${statsLoading ? '-:-' : totalSMSCount} SMS Sent`}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</SimpleGrid>
|
||||
<Box maxW='12xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
|
||||
<Grid
|
||||
templateColumns={{ base: 'repeat(1, 1fr)', md: 'repeat(3, 1fr)' }}
|
||||
gap={6}
|
||||
>
|
||||
<GridItem colSpan={1}>
|
||||
<chakra.h1
|
||||
textAlign={'center'}
|
||||
fontSize={'2xl'}
|
||||
py={10}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
Welcome {authUser?.name}
|
||||
</chakra.h1>
|
||||
</GridItem>
|
||||
<GridItem colSpan={2}>
|
||||
<SimpleGrid
|
||||
columns={{ base: 2, md: 4 }}
|
||||
spacing={{ base: 5, lg: 8 }}
|
||||
>
|
||||
<UserStatsCard
|
||||
title={'Registered '}
|
||||
stat={`${statsLoading ? '-:-' : totalDeviceCount} Devices`}
|
||||
/>
|
||||
<UserStatsCard
|
||||
title={'Generated'}
|
||||
stat={`${statsLoading ? '-:-' : totalApiKeyCount} API Keys`}
|
||||
/>
|
||||
<UserStatsCard
|
||||
title={'Sent'}
|
||||
stat={`${statsLoading ? '-:-' : totalSentSMSCount} SMS Sent`}
|
||||
/>
|
||||
<UserStatsCard
|
||||
title={'Received'}
|
||||
stat={`${
|
||||
statsLoading ? '-:-' : totalReceivedSMSCount
|
||||
} SMS Received`}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -10,20 +10,21 @@ export default function UserStatsCard({ ...props }) {
|
||||
const { title, stat } = props
|
||||
return (
|
||||
<Stat
|
||||
px={{ base: 4, md: 8 }}
|
||||
py={'5'}
|
||||
px={{ base: 2, md: 4 }}
|
||||
py={'3'}
|
||||
shadow={'xl'}
|
||||
border={'1px solid'}
|
||||
borderColor={useColorModeValue('gray.800', 'gray.500')}
|
||||
rounded={'lg'}
|
||||
style={{
|
||||
height: '90px'
|
||||
height: '90px',
|
||||
}}
|
||||
alignContent={'center'}
|
||||
>
|
||||
<StatLabel fontWeight={'medium'} isTruncated>
|
||||
{title}
|
||||
</StatLabel>
|
||||
<StatNumber fontSize={'md'} fontWeight={'bold'}>
|
||||
<StatNumber fontSize={'md'} fontWeight={'medium'}>
|
||||
{stat}
|
||||
</StatNumber>
|
||||
</Stat>
|
||||
|
||||
@@ -10,9 +10,13 @@ export default function CodeSnippetSection() {
|
||||
const API_KEY = 'YOUR_API_KEY'
|
||||
const DEVICE_ID = 'YOUR_DEVICE_ID'
|
||||
|
||||
await axios.post(\`\$\{BASE_URL\}/gateway/devices/\$\{DEVICE_ID}/sendSMS?apiKey=\$\{API_KEY\}\`, {
|
||||
receivers: [ '+251912345678' ],
|
||||
smsBody: 'Hello World!',
|
||||
await axios.post(\`\$\{BASE_URL\}/gateway/devices/\$\{DEVICE_ID}/sendSMS\`, {
|
||||
recipients: [ '+251912345678' ],
|
||||
message: 'Hello World!',
|
||||
}, {
|
||||
headers: {
|
||||
'x-api-key': API_KEY,
|
||||
},
|
||||
})
|
||||
|
||||
`
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function DownloadAppSection() {
|
||||
Unlock the power of messaging with our open-source Android SMS
|
||||
Gateway.
|
||||
</chakra.p>
|
||||
<a href='/android' target='_blank'>
|
||||
<a href='https://dl.textbee.dev' target='_blank'>
|
||||
<Button
|
||||
/* flex={1} */
|
||||
px={4}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const howItWorksContent = [
|
||||
{
|
||||
title: 'Step 1: Download The Android App from textbee.dev/android',
|
||||
title: 'Step 1: Download The Android App from dl.textbee.dev',
|
||||
description:
|
||||
'',
|
||||
},
|
||||
|
||||
@@ -5,9 +5,19 @@ export default function Meta() {
|
||||
<Head>
|
||||
<title>TextBee - SMS Gateway</title>
|
||||
<meta name='viewport' content='initial-scale=1.0, width=device-width' />
|
||||
<meta name='description' content='Android SMS Gateway' />
|
||||
<meta name='keywords' content='android, text, sms, gateway, sms-gateway' />
|
||||
<meta name='author' content='Israel Abebe' />
|
||||
<meta
|
||||
name='description'
|
||||
content={`TextBee is an open-source SMS gateway platform built for Android devices.
|
||||
It allows businesses to send SMS messages from dashboard or API, receiving SMS messages and forwarding them to a webhook,
|
||||
streamlining communication and automating SMS workflows. With its robust features,
|
||||
TextBee is an ideal solution for CRM's, notifications, alerts, two-factor authentication, and various other use cases.
|
||||
`}
|
||||
/>
|
||||
<meta
|
||||
name='keywords'
|
||||
content='android, text, sms, gateway, sms-gateway, open-source foss'
|
||||
/>
|
||||
<meta name='author' content='Israel Abebe Kokiso' />
|
||||
<link rel='icon' href='/favicon.ico' />
|
||||
</Head>
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ const nextConfig = {
|
||||
return [
|
||||
{
|
||||
source: '/android',
|
||||
destination: 'https://appdistribution.firebase.dev/i/1439f7af2d1e8e8e',
|
||||
destination: 'https://dl.textbee.dev',
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Box, SimpleGrid, useToast } from '@chakra-ui/react'
|
||||
import ApiKeyList from '../components/dashboard/ApiKeyList'
|
||||
import {
|
||||
Box,
|
||||
Tab,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Tabs,
|
||||
useToast,
|
||||
} from '@chakra-ui/react'
|
||||
import UserStats from '../components/dashboard/UserStats'
|
||||
import GenerateApiKey from '../components/dashboard/GenerateApiKey'
|
||||
import DeviceList from '../components/dashboard/DeviceList'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectAuthUser } from '../store/authSlice'
|
||||
import Router from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import SendSMS from '../components/dashboard/SendSMS'
|
||||
import ErrorBoundary from '../components/ErrorBoundary'
|
||||
import dynamic from 'next/dynamic'
|
||||
import ReceiveSMS from '../components/dashboard/ReceiveSMS'
|
||||
import APIKeyAndDevices from '../components/dashboard/APIKeyAndDevices'
|
||||
|
||||
export default function Dashboard() {
|
||||
const NoSSRAnimatedWrapper = dynamic(
|
||||
@@ -31,25 +37,39 @@ export default function Dashboard() {
|
||||
Router.push('/login')
|
||||
}
|
||||
}, [authUser, toast])
|
||||
|
||||
return (
|
||||
<NoSSRAnimatedWrapper>
|
||||
<UserStats />
|
||||
<Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
|
||||
<SimpleGrid columns={{ base: 1, md: 2 }} spacing={{ base: 5, lg: 8 }}>
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
|
||||
<GenerateApiKey />
|
||||
<ErrorBoundary>
|
||||
<ApiKeyList />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
<Box backdropBlur='2xl' borderWidth='0px' borderRadius='lg'>
|
||||
<SendSMS />
|
||||
<ErrorBoundary>
|
||||
<DeviceList />
|
||||
</ErrorBoundary>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
</NoSSRAnimatedWrapper>
|
||||
<>
|
||||
<NoSSRAnimatedWrapper>
|
||||
<UserStats />
|
||||
<DashboardTabView />
|
||||
</NoSSRAnimatedWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DashboardTabView = () => {
|
||||
const [tabIndex, setTabIndex] = useState(0)
|
||||
return (
|
||||
<Box maxW='7xl' mx={'auto'} pt={5} px={{ base: 2, sm: 12, md: 17 }}>
|
||||
<Tabs isLazy={false} index={tabIndex} onChange={setTabIndex}>
|
||||
<TabList>
|
||||
<Tab>API Key and Devices</Tab>
|
||||
<Tab>Send SMS</Tab>
|
||||
<Tab>Receive SMS</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<APIKeyAndDevices />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SendSMS />
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<ReceiveSMS />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,11 @@ class AuthService {
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
async whoAmI() {
|
||||
const res = await httpClient.get(`/auth/who-am-i`)
|
||||
return res.data.data
|
||||
}
|
||||
}
|
||||
|
||||
export const authService = new AuthService()
|
||||
|
||||
@@ -34,6 +34,11 @@ class GatewayService {
|
||||
)
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
async getReceivedSMSList(deviceId: string) {
|
||||
const res = await httpClient.get(`/gateway/devices/${deviceId}/getReceivedSMS`)
|
||||
return res.data.data
|
||||
}
|
||||
}
|
||||
|
||||
export const gatewayService = new GatewayService()
|
||||
|
||||
@@ -50,8 +50,8 @@ export interface CurrentUserResponse extends BaseResponse {
|
||||
}
|
||||
|
||||
export interface SendSMSRequestPayload {
|
||||
receivers: string[]
|
||||
smsBody: string
|
||||
recipients: string[]
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface ApiKeyEntity {
|
||||
|
||||
@@ -11,6 +11,10 @@ const initialState = {
|
||||
item: null,
|
||||
list: [],
|
||||
sendingSMS: false,
|
||||
receivedSMSList: {
|
||||
loading: false,
|
||||
data: [],
|
||||
},
|
||||
}
|
||||
|
||||
export const fetchDevices = createAsyncThunk(
|
||||
@@ -29,6 +33,22 @@ export const fetchDevices = createAsyncThunk(
|
||||
}
|
||||
)
|
||||
|
||||
export const fetchReceivedSMSList = createAsyncThunk(
|
||||
'device/fetchReceivedSMSList',
|
||||
async (id: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const res = await gatewayService.getReceivedSMSList(id)
|
||||
return res
|
||||
} catch (e) {
|
||||
toast({
|
||||
title: e.response.data.error || 'Failed to Fetch received sms list',
|
||||
status: 'error',
|
||||
})
|
||||
return rejectWithValue(e.response.data)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const deleteDevice = createAsyncThunk(
|
||||
'device/deleteDevice',
|
||||
async (id: string, { rejectWithValue, dispatch }) => {
|
||||
@@ -100,6 +120,19 @@ export const deviceSlice = createSlice({
|
||||
.addCase(sendSMS.rejected, (state) => {
|
||||
state.sendingSMS = false
|
||||
})
|
||||
.addCase(fetchReceivedSMSList.pending, (state) => {
|
||||
state.receivedSMSList.loading = true
|
||||
})
|
||||
.addCase(
|
||||
fetchReceivedSMSList.fulfilled,
|
||||
(state, action: PayloadAction<any>) => {
|
||||
state.receivedSMSList.loading = false
|
||||
state.receivedSMSList.data = action.payload
|
||||
}
|
||||
)
|
||||
.addCase(fetchReceivedSMSList.rejected, (state) => {
|
||||
state.receivedSMSList.loading = false
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -109,5 +142,7 @@ export const selectDeviceList = (state: RootState) => state.device.list
|
||||
export const selectDeviceItem = (state: RootState) => state.device.item
|
||||
export const selectDeviceLoading = (state: RootState) => state.device.loading
|
||||
export const selectSendingSMS = (state: RootState) => state.device.sendingSMS
|
||||
export const selectReceivedSMSList = (state: RootState) =>
|
||||
state.device.receivedSMSList
|
||||
|
||||
export default deviceSlice.reducer
|
||||
|
||||
@@ -11,7 +11,8 @@ const initialState = {
|
||||
data: {
|
||||
totalApiKeyCount: 0,
|
||||
totalDeviceCount: 0,
|
||||
totalSMSCount: 0,
|
||||
totalReceivedSMSCount: 0,
|
||||
totalSentSMSCount: 0,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user