in-app-purchase: pro-mode

This commit is contained in:
Mike Schwörer 2018-11-12 00:15:50 +01:00
parent e69b1232d7
commit 083945852b
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
38 changed files with 1134 additions and 62 deletions

View File

@ -35,7 +35,7 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.0.0'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
@ -45,7 +45,8 @@ dependencies {
implementation 'com.google.android.material:material:1.0.0'
implementation 'com.google.firebase:firebase-core:16.0.4'
implementation 'com.google.firebase:firebase-messaging:17.3.4'
implementation 'com.google.android.gms:play-services-ads:17.0.0'
implementation 'com.google.android.gms:play-services-ads:17.1.0'
implementation 'com.android.billingclient:billing:1.2'
implementation 'com.squareup.okhttp3:okhttp:3.10.0'
implementation 'com.github.kenglxn.QRGen:android:2.5.0'

View File

@ -4,6 +4,7 @@ import android.app.Application;
import android.content.Context;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.blackforestbytes.simplecloudnotifier.view.AccountFragment;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import com.blackforestbytes.simplecloudnotifier.view.TabAdapter;

View File

@ -0,0 +1,23 @@
package com.blackforestbytes.simplecloudnotifier.lib.android;
import android.util.Log;
public final class ThreadUtils
{
public static void safeSleep(int millisMin, int millisMax)
{
safeSleep(millisMin + (int)(Math.random()*(millisMax-millisMin)));
}
public static void safeSleep(int millis)
{
try
{
Thread.sleep(millis);
}
catch (InterruptedException e)
{
Log.d("ThreadUtils", e.toString());
}
}
}

View File

@ -0,0 +1,38 @@
package com.blackforestbytes.simplecloudnotifier.lib.collections;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func1to1;
import java.util.*;
public final class CollectionHelper
{
public static <T, C> List<T> unique(List<T> input, Func1to1<T, C> mapping)
{
List<T> output = new ArrayList<>(input.size());
HashSet<C> seen = new HashSet<>();
for (T v : input) if (seen.add(mapping.invoke(v))) output.add(v);
return output;
}
public static <T> List<T> sort(List<T> input, Comparator<T> comparator)
{
List<T> output = new ArrayList<>(input);
Collections.sort(output, comparator);
return output;
}
public static <T, U extends Comparable<U>> List<T> sort(List<T> input, Func1to1<T, U> mapper)
{
return sort(input, mapper, 1);
}
public static <T, U extends Comparable<U>> List<T> sort(List<T> input, Func1to1<T, U> mapper, int sortMod)
{
List<T> output = new ArrayList<>(input);
Collections.sort(output, (o1, o2) -> sortMod * mapper.invoke(o1).compareTo(mapper.invoke(o2)));
return output;
}
}

View File

@ -0,0 +1,14 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class IntRange
{
private int Start;
public int Start() { return Start; }
private int End;
public int End() { return End; }
public IntRange(int s, int e) { Start = s; End = e; }
private IntRange() { }
}

View File

@ -0,0 +1,10 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class NInt
{
public int Value;
public NInt(int v) { Value = v; }
private NInt() { }
}

View File

@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public final class Nothing
{
public final static Nothing Inst = new Nothing();
}

View File

@ -0,0 +1,11 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class Tuple1<T1>
{
public final T1 Item1;
public Tuple1(T1 i1)
{
Item1 = i1;
}
}

View File

@ -0,0 +1,13 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class Tuple2<T1, T2>
{
public final T1 Item1;
public final T2 Item2;
public Tuple2(T1 i1, T2 i2)
{
Item1 = i1;
Item2 = i2;
}
}

View File

@ -0,0 +1,15 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class Tuple3<T1, T2, T3>
{
public final T1 Item1;
public final T2 Item2;
public final T3 Item3;
public Tuple3(T1 i1, T2 i2, T3 i3)
{
Item1 = i1;
Item2 = i2;
Item3 = i3;
}
}

View File

@ -0,0 +1,17 @@
package com.blackforestbytes.simplecloudnotifier.lib.datatypes;
public class Tuple4<T1, T2, T3, T4>
{
public final T1 Item1;
public final T2 Item2;
public final T3 Item3;
public final T4 Item4;
public Tuple4(T1 i1, T2 i2, T3 i3, T4 i4)
{
Item1 = i1;
Item2 = i2;
Item3 = i3;
Item4 = i4;
}
}

View File

@ -0,0 +1,9 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func0to0 {
Func0to0 EMPTY = ()->{};
void invoke();
}

View File

@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func0to1<TResult> {
TResult invoke();
}

View File

@ -0,0 +1,8 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
import java.io.IOException;
@FunctionalInterface
public interface Func0to1WithIOException<TResult> {
TResult invoke() throws IOException;
}

View File

@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func1to0<TInput1> {
void invoke(TInput1 value);
}

View File

@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func1to1<TInput1, TResult> {
TResult invoke(TInput1 value);
}

View File

@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func2to0<TInput1, TInput2> {
void invoke(TInput1 value1, TInput2 value2);
}

View File

@ -0,0 +1,6 @@
package com.blackforestbytes.simplecloudnotifier.lib.lambda;
@FunctionalInterface
public interface Func2to1<TInput1, TInput2, TResult> {
TResult invoke(TInput1 value1, TInput2 value2);
}

View File

@ -0,0 +1,231 @@
package com.blackforestbytes.simplecloudnotifier.lib.string;
// from MonoSAMFramework.Portable.DebugTools.CompactJsonFormatter
public class CompactJsonFormatter
{
private static final String INDENT_STRING = " ";
public static String formatJSON(String str, int maxIndent)
{
int indent = 0;
boolean quoted = false;
StringBuilder sb = new StringBuilder();
char last = ' ';
for (int i = 0; i < str.length(); i++)
{
char ch = str.charAt(i);
switch (ch)
{
case '\r':
case '\n':
break;
case '{':
case '[':
sb.append(ch);
last = ch;
if (!quoted)
{
indent++;
if (indent >= maxIndent) break;
sb.append("\n");
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
last = ' ';
}
break;
case '}':
case ']':
if (!quoted)
{
indent--;
if (indent + 1 >= maxIndent) { sb.append(ch); break; }
sb.append("\n");
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
}
sb.append(ch);
last = ch;
break;
case '"':
sb.append(ch);
last = ch;
boolean escaped = false;
int index = i;
while (index > 0 && str.charAt(--index) == '\\')
escaped = !escaped;
if (!escaped)
quoted = !quoted;
break;
case ',':
sb.append(ch);
last = ch;
if (!quoted)
{
if (indent >= maxIndent) { sb.append(' '); last = ' '; break; }
sb.append("\n");
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
}
break;
case ':':
sb.append(ch);
last = ch;
if (!quoted) { sb.append(" "); last = ' '; }
break;
case ' ':
case '\t':
if (quoted)
{
sb.append(ch);
last = ch;
}
else if (last != ' ')
{
sb.append(' ');
last = ' ';
}
break;
default:
sb.append(ch);
last = ch;
break;
}
}
return sb.toString();
}
public static String compressJson(String str, int compressionLevel)
{
int indent = 0;
boolean quoted = false;
StringBuilder sb = new StringBuilder();
char last = ' ';
int compress = 0;
for (int i = 0; i < str.length(); i++)
{
char ch = str.charAt(i);
switch (ch)
{
case '\r':
case '\n':
break;
case '{':
case '[':
sb.append(ch);
last = ch;
if (!quoted)
{
if (compress == 0 && getJsonDepth(str, i) <= compressionLevel)
compress = 1;
else if (compress > 0)
compress++;
indent++;
if (compress > 0) break;
sb.append("\n");
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
last = ' ';
}
break;
case '}':
case ']':
if (!quoted)
{
indent--;
if (compress > 0) { compress--; sb.append(ch); break; }
compress--;
sb.append("\n");
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
}
sb.append(ch);
last = ch;
break;
case '"':
sb.append(ch);
last = ch;
boolean escaped = false;
int index = i;
while (index > 0 && str.charAt(--index) == '\\')
escaped = !escaped;
if (!escaped)
quoted = !quoted;
break;
case ',':
sb.append(ch);
last = ch;
if (!quoted)
{
if (compress > 0) { sb.append(' '); last = ' '; break; }
sb.append("\n");
for (int ix = 0; ix < indent; ix++) sb.append(INDENT_STRING);
}
break;
case ':':
sb.append(ch);
last = ch;
if (!quoted) { sb.append(" "); last = ' '; }
break;
case ' ':
case '\t':
if (quoted)
{
sb.append(ch);
last = ch;
}
else if (last != ' ')
{
sb.append(' ');
last = ' ';
}
break;
default:
sb.append(ch);
last = ch;
break;
}
}
return sb.toString();
}
public static int getJsonDepth(String str, int i)
{
int maxindent = 0;
int indent = 0;
boolean quoted = false;
for (; i < str.length(); i++)
{
char ch = str.charAt(i);
switch (ch)
{
case '{':
case '[':
if (!quoted)
{
indent++;
maxindent = Math.max(indent, maxindent);
}
break;
case '}':
case ']':
if (!quoted)
{
indent--;
if (indent <= 0) return maxindent;
}
break;
case '"':
boolean escaped = false;
int index = i;
while (index > 0 && str.charAt(--index) == '\\')
escaped = !escaped;
if (!escaped)
quoted = !quoted;
break;
default:
break;
}
}
return maxindent;
}
}

View File

@ -0,0 +1,98 @@
package com.blackforestbytes.simplecloudnotifier.lib.string;
import android.content.Context;
import android.util.Log;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func1to1;
import java.text.MessageFormat;
import java.util.List;
public class Str
{
public final static String Empty = "";
public static String format(String fmt, Object... data)
{
return MessageFormat.format(fmt, data);
}
public static String rformat(int fmtResId, Object... data)
{
Context inst = SCNApp.getContext();
if (inst == null)
{
Log.e("StringFormat", "rformat::NoInstance --> inst==null for" + fmtResId);
return "?ERR?";
}
return MessageFormat.format(inst.getResources().getString(fmtResId), data);
}
public static String firstLine(String content)
{
int idx = content.indexOf('\n');
if (idx == -1) return content;
if (idx == 0) return Str.Empty;
if (content.charAt(idx-1) == '\r') return content.substring(0, idx-1);
return content.substring(0, idx);
}
public static boolean isNullOrWhitespace(String str)
{
return str == null || str.length() == 0 || str.trim().length() == 0;
}
public static boolean isNullOrEmpty(String str)
{
return str == null || str.length() == 0;
}
public static boolean equals(String a, String b)
{
if (a == null) return (b == null);
return a.equals(b);
}
public static String join(String sep, List<String> list)
{
StringBuilder b = new StringBuilder();
boolean first = true;
for (String v : list)
{
if (!first) b.append(sep);
b.append(v);
first = false;
}
return b.toString();
}
public static <T> String join(String sep, List<T> list, Func1to1<T, String> map)
{
StringBuilder b = new StringBuilder();
boolean first = true;
for (T v : list)
{
if (!first) b.append(sep);
b.append(map.invoke(v));
first = false;
}
return b.toString();
}
public static Integer tryParseToInt(String s)
{
try
{
return Integer.parseInt(s);
}
catch (Exception e)
{
return null;
}
}
}

View File

@ -6,7 +6,9 @@ import android.content.SharedPreferences;
import android.util.Log;
import android.view.View;
import com.android.billingclient.api.Purchase;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.google.firebase.iid.FirebaseInstanceId;
public class SCNSettings
@ -36,6 +38,10 @@ public class SCNSettings
public String fcm_token_local;
public String fcm_token_server;
public String promode_token;
public boolean promode_local;
public boolean promode_server;
// ------------------------------------------------------------
public boolean Enabled = true;
@ -57,6 +63,9 @@ public class SCNSettings
user_key = sharedPref.getString("user_key", "");
fcm_token_local = sharedPref.getString("fcm_token_local", "");
fcm_token_server = sharedPref.getString("fcm_token_server", "");
promode_local = sharedPref.getBoolean("promode_local", false);
promode_server = sharedPref.getBoolean("promode_server", false);
promode_token = sharedPref.getString("promode_token", "");
Enabled = sharedPref.getBoolean("app_enabled", Enabled);
LocalCacheSize = sharedPref.getInt("local_cache_size", LocalCacheSize);
@ -151,10 +160,12 @@ public class SCNSettings
{
fcm_token_local = token;
save();
ServerCommunication.register(fcm_token_local, loader);
ServerCommunication.register(fcm_token_local, loader, promode_local, promode_token);
updateProState(loader);
}
}
// called at app start
public void work(Activity a)
{
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
@ -166,8 +177,11 @@ public class SCNSettings
{
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
});
updateProState(null);
}
// reset account key
public void reset(View loader)
{
if (!isConnected()) return;
@ -175,23 +189,50 @@ public class SCNSettings
ServerCommunication.update(user_id, user_key, loader);
}
// refresh account data
public void refresh(View loader, Activity a)
{
if (isConnected())
{
ServerCommunication.info(user_id, user_key, loader);
if (promode_server != promode_local)
{
updateProState(loader);
}
}
else
{
// get token then register
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener(a, instanceIdResult ->
{
String newToken = instanceIdResult.getToken();
Log.e("FB::GetInstanceId", newToken);
SCNSettings.inst().setServerToken(newToken, loader);
SCNSettings.inst().setServerToken(newToken, loader); // does register in here
}).addOnCompleteListener(r ->
{
if (isConnected()) ServerCommunication.info(user_id, user_key, null);
if (isConnected()) ServerCommunication.info(user_id, user_key, null); // info again for safety
});
}
}
public void updateProState(View loader)
{
Purchase purch = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
boolean promode_real = (purch != null);
if (promode_real != promode_local || promode_real != promode_server)
{
promode_local = promode_real;
promode_token = promode_real ? purch.getPurchaseToken() : "";
updateProStateOnServer(loader);
}
}
public void updateProStateOnServer(View loader)
{
if (!isConnected()) return;
ServerCommunication.upgrade(user_id, user_key, loader, promode_local, promode_token);
}
}

View File

@ -9,6 +9,7 @@ import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.IOException;
import java.net.URLEncoder;
import okhttp3.Call;
import okhttp3.Callback;
@ -25,12 +26,12 @@ public class ServerCommunication
private ServerCommunication(){ throw new Error("no."); }
public static void register(String token, View loader)
public static void register(String token, View loader, boolean pro, String pro_token)
{
try
{
Request request = new Request.Builder()
.url(BASE_URL + "register.php?fcm_token="+token)
.url(BASE_URL + "register.php?fcm_token=" + token + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
.build();
client.newCall(request).enqueue(new Callback()
@ -67,6 +68,7 @@ public class ServerCommunication
SCNSettings.inst().fcm_token_server = token;
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().promode_server = json.getBoolean("is_pro");
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
@ -132,6 +134,7 @@ public class ServerCommunication
SCNSettings.inst().fcm_token_server = token;
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().promode_server = json.getBoolean("is_pro");
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
@ -187,10 +190,11 @@ public class ServerCommunication
return;
}
SCNSettings.inst().user_id = json.getInt("user_id");
SCNSettings.inst().user_key = json.getString("user_key");
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().user_id = json.getInt("user_id");
SCNSettings.inst().user_key = json.getString("user_key");
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().promode_server = json.getBoolean("is_pro");
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
@ -247,9 +251,10 @@ public class ServerCommunication
return;
}
SCNSettings.inst().user_id = json.getInt("user_id");
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().user_id = json.getInt("user_id");
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().promode_server = json.getBoolean("is_pro");
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
@ -270,4 +275,63 @@ public class ServerCommunication
SCNApp.showToast("Communication with server failed", 4000);
}
}
public static void upgrade(int id, String key, View loader, boolean pro, String pro_token)
{
try
{
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
Request request = new Request.Builder()
.url(BASE_URL + "upgrade.php?user_id=" + id + "&user_key=" + key + "&pro=" + pro + "&pro_token=" + URLEncoder.encode(pro_token, "utf-8"))
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
SCNApp.showToast("Communication with server failed", 4000);
}
@Override
public void onResponse(Call call, Response response) {
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful())
throw new IOException("Unexpected code " + response);
if (responseBody == null) throw new IOException("No response");
String r = responseBody.string();
Log.d("Server::Response", r);
JSONObject json = (JSONObject) new JSONTokener(r).nextValue();
if (!json.getBoolean("success")) {
SCNApp.showToast(json.getString("message"), 4000);
return;
}
SCNSettings.inst().user_id = json.getInt("user_id");
SCNSettings.inst().user_key = json.getString("user_key");
SCNSettings.inst().quota_curr = json.getInt("quota");
SCNSettings.inst().quota_max = json.getInt("quota_max");
SCNSettings.inst().promode_server = json.getBoolean("is_pro");
SCNSettings.inst().save();
SCNApp.refreshAccountTab();
} catch (Exception e) {
e.printStackTrace();
SCNApp.showToast("Communication with server failed", 4000);
} finally {
SCNApp.runOnUiThread(() -> { if (loader != null) loader.setVisibility(View.GONE); });
}
}
});
}
catch (Exception e)
{
e.printStackTrace();
SCNApp.showToast("Communication with server failed", 4000);
}
}
}

View File

@ -0,0 +1,195 @@
package com.blackforestbytes.simplecloudnotifier.service;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.lib.lambda.Func0to0;
import com.blackforestbytes.simplecloudnotifier.lib.string.Str;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.view.MainActivity;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.Nullable;
import static androidx.constraintlayout.widget.Constraints.TAG;
public class IABService implements PurchasesUpdatedListener
{
public static final String IAB_PRO_MODE = "scn.pro.tier1";
private final static Object _lock = new Object();
private static IABService _inst = null;
public static IABService inst()
{
synchronized (_lock)
{
if (_inst != null) return _inst;
throw new Error("IABService == null");
}
}
public static void startup(MainActivity a)
{
synchronized (_lock)
{
_inst = new IABService(a);
}
}
private BillingClient client;
private boolean isServiceConnected;
private final List<Purchase> purchases = new ArrayList<>();
public IABService(Context c)
{
client = BillingClient
.newBuilder(c)
.setListener(this)
.build();
startServiceConnection(this::queryPurchases, false);
}
public void queryPurchases()
{
Func0to0 queryToExecute = () ->
{
long time = System.currentTimeMillis();
Purchase.PurchasesResult purchasesResult = client.queryPurchases(BillingClient.SkuType.INAPP);
Log.i(TAG, "Querying purchases elapsed time: " + (System.currentTimeMillis() - time) + "ms");
if (purchasesResult.getResponseCode() == BillingClient.BillingResponse.OK)
{
for (Purchase p : purchasesResult.getPurchasesList())
{
handlePurchase(p);
}
boolean newProMode = getPurchaseCached(IAB_PRO_MODE) != null;
if (newProMode != SCNSettings.inst().promode_local)
{
refreshProModeListener();
}
}
else
{
Log.w(TAG, "queryPurchases() got an error response code: " + purchasesResult.getResponseCode());
}
};
executeServiceRequest(queryToExecute, false);
}
public void purchase(Activity a, String id)
{
executeServiceRequest(() ->
{
BillingFlowParams flowParams = BillingFlowParams
.newBuilder()
.setSku(id)
.setType(BillingClient.SkuType.INAPP) // SkuType.SUB for subscription
.build();
client.launchBillingFlow(a, flowParams);
}, true);
}
private void executeServiceRequest(Func0to0 runnable, final boolean userRequest) {
if (isServiceConnected)
{
runnable.invoke();
}
else
{
// If billing service was disconnected, we try to reconnect 1 time.
// (feel free to introduce your retry policy here).
startServiceConnection(runnable, userRequest);
}
}
public void destroy()
{
if (client != null && client.isReady()) {
client.endConnection();
client = null;
isServiceConnected = false;
}
}
@Override
public void onPurchasesUpdated(int responseCode, @Nullable List<Purchase> purchases)
{
if (responseCode == BillingClient.BillingResponse.OK && purchases != null)
{
for (Purchase purchase : purchases)
{
handlePurchase(purchase);
}
}
else if (responseCode == BillingClient.BillingResponse.ITEM_ALREADY_OWNED && purchases != null)
{
for (Purchase purchase : purchases)
{
handlePurchase(purchase);
}
}
}
private void handlePurchase(Purchase purchase)
{
Log.d(TAG, "Got a verified purchase: " + purchase);
purchases.add(purchase);
refreshProModeListener();
}
private void refreshProModeListener() {
MainActivity ma = SCNApp.getMainActivity();
if (ma != null) ma.adpTabs.tab3.updateProState();
if (ma != null) ma.adpTabs.tab1.updateProState();
SCNSettings.inst().updateProState(null);
}
public void startServiceConnection(final Func0to0 executeOnSuccess, final boolean userRequest)
{
client.startConnection(new BillingClientStateListener()
{
@Override
public void onBillingSetupFinished(@BillingClient.BillingResponse int billingResponseCode)
{
if (billingResponseCode == BillingClient.BillingResponse.OK)
{
isServiceConnected = true;
if (executeOnSuccess != null) executeOnSuccess.invoke();
}
else
{
if (userRequest) SCNApp.showToast("Could not connect to google services", Toast.LENGTH_SHORT);
}
}
@Override
public void onBillingServiceDisconnected() {
isServiceConnected = false;
}
});
}
public Purchase getPurchaseCached(String id)
{
for (Purchase p : purchases)
{
if (Str.equals(p.getSku(), id)) return p;
}
return null;
}
}

View File

@ -6,8 +6,6 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;

View File

@ -1,17 +1,15 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.net.Uri;
import android.os.Bundle;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.SCNApp;
import com.blackforestbytes.simplecloudnotifier.model.CMessageList;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
import com.google.android.material.tabs.TabLayout;
import org.jetbrains.annotations.NotNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.viewpager.widget.PagerAdapter;
@ -42,7 +40,7 @@ public class MainActivity extends AppCompatActivity
tabLayout.setupWithViewPager(viewPager);
SCNApp.register(this);
IABService.startup(this);
SCNSettings.inst().work(this);
}
@ -51,6 +49,16 @@ public class MainActivity extends AppCompatActivity
{
super.onStop();
SCNSettings.inst().save();
CMessageList.inst().fullSave();
}
@Override
protected void onDestroy()
{
super.onDestroy();
CMessageList.inst().fullSave();
IABService.inst().destroy();
}
}

View File

@ -6,6 +6,8 @@ import android.view.View;
import android.view.ViewGroup;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.google.android.gms.ads.doubleclick.PublisherAdRequest;
import com.google.android.gms.ads.doubleclick.PublisherAdView;
@ -16,6 +18,8 @@ import androidx.recyclerview.widget.RecyclerView;
public class NotificationsFragment extends Fragment
{
private PublisherAdView adView;
public NotificationsFragment()
{
// Required empty public constructor
@ -30,11 +34,17 @@ public class NotificationsFragment extends Fragment
rvMessages.setLayoutManager(new LinearLayoutManager(this.getContext(), RecyclerView.VERTICAL, true));
rvMessages.setAdapter(new MessageAdapter(v.findViewById(R.id.tvNoElements)));
PublisherAdView mPublisherAdView = v.findViewById(R.id.adBanner);
adView = v.findViewById(R.id.adBanner);
PublisherAdRequest adRequest = new PublisherAdRequest.Builder().build();
mPublisherAdView.loadAd(adRequest);
adView.loadAd(adRequest);
adView.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
return v;
}
public void updateProState()
{
adView.setVisibility(IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE) != null ? View.GONE : View.VISIBLE);
}
}

View File

@ -1,6 +1,5 @@
package com.blackforestbytes.simplecloudnotifier.view;
import android.app.NotificationManager;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
@ -16,8 +15,10 @@ import android.widget.Spinner;
import android.widget.Switch;
import android.widget.TextView;
import com.android.billingclient.api.Purchase;
import com.blackforestbytes.simplecloudnotifier.R;
import com.blackforestbytes.simplecloudnotifier.model.SCNSettings;
import com.blackforestbytes.simplecloudnotifier.service.IABService;
import com.blackforestbytes.simplecloudnotifier.service.NotificationService;
import org.jetbrains.annotations.NotNull;
@ -33,6 +34,8 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private Switch prefAppEnabled;
private Spinner prefLocalCacheSize;
private Button prefUpgradeAccount;
private TextView prefUpgradeAccount_msg;
private TextView prefUpgradeAccount_info;
private Switch prefMsgLowEnableSound;
private TextView prefMsgLowRingtone_value;
@ -85,6 +88,8 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
prefAppEnabled = v.findViewById(R.id.prefAppEnabled);
prefLocalCacheSize = v.findViewById(R.id.prefLocalCacheSize);
prefUpgradeAccount = v.findViewById(R.id.prefUpgradeAccount);
prefUpgradeAccount_msg = v.findViewById(R.id.prefUpgradeAccount2);
prefUpgradeAccount_info = v.findViewById(R.id.prefUpgradeAccount_info);
prefMsgLowEnableSound = v.findViewById(R.id.prefMsgLowEnableSound);
prefMsgLowRingtone_value = v.findViewById(R.id.prefMsgLowRingtone_value);
@ -122,6 +127,10 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
if (prefAppEnabled.isChecked() != s.Enabled) prefAppEnabled.setChecked(s.Enabled);
prefUpgradeAccount.setVisibility( SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
prefUpgradeAccount_info.setVisibility(SCNSettings.inst().promode_local ? View.GONE : View.VISIBLE);
prefUpgradeAccount_msg.setVisibility( SCNSettings.inst().promode_local ? View.VISIBLE : View.GONE );
ArrayAdapter<Integer> plcsa = new ArrayAdapter<>(c, android.R.layout.simple_spinner_item, SCNSettings.CHOOSABLE_CACHE_SIZES);
plcsa.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
prefLocalCacheSize.setAdapter(plcsa);
@ -197,7 +206,16 @@ public class SettingsFragment extends Fragment implements MusicPickerListener
private void onUpgradeAccount()
{
//TODO
IABService.inst().purchase(getActivity(), IABService.IAB_PRO_MODE);
}
public void updateProState()
{
Purchase p = IABService.inst().getPurchaseCached(IABService.IAB_PRO_MODE);
prefUpgradeAccount.setVisibility( p != null ? View.GONE : View.VISIBLE);
prefUpgradeAccount_info.setVisibility(p != null ? View.GONE : View.VISIBLE);
prefUpgradeAccount_msg.setVisibility( p != null ? View.VISIBLE : View.GONE );
}
private int getCacheSizeIndex(int value)

View File

@ -96,7 +96,8 @@
android:layout_marginTop="2dp"
android:background="#c0c0c0"/>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
android:orientation="vertical"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:layout_width="match_parent"
@ -107,12 +108,25 @@
android:id="@+id/prefUpgradeAccount"
android:text="@string/str_upgrade_account"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/prefUpgradeAccount_info"
android:textAlignment="center"
android:text="@string/str_promode_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/prefUpgradeAccount2"
android:textColor="#FF4D00"
android:textStyle="bold"
android:textAlignment="center"
android:text="@string/str_promode"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>

View File

@ -28,4 +28,6 @@
<string name="str_ledcolor">Notification light color</string>
<string name="str_enable_vibration">Enable notification vibration</string>
<string name="str_upgrade_account">Upgrade account</string>
<string name="str_promode">Thank you for supporting the app and using the pro mode</string>
<string name="str_promode_info">Increase your daily quota, remove the ad banner and support the developer (that\'s me)</string>
</resources>

View File

@ -8,7 +8,7 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.google.gms:google-services:4.0.1'
classpath 'com.google.gms:google-services:4.2.0'
}
}

3
web/.gitignore vendored
View File

@ -180,4 +180,5 @@ $RECYCLE.BIN/
#################
config.php
config.php
.verify_accesstoken

View File

@ -15,7 +15,7 @@ $user_key = $INPUT['user_key'];
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1');
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, is_pro, quota_day FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
@ -27,16 +27,17 @@ if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'er
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'errid'=>204, 'message' => 'Authentification failed']));
$quota = $data['quota_today'];
$quota_max = $data['quota_max'];
$is_pro = $data['is_pro'];
if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $quota=0;
echo json_encode(
[
'success' => true,
'user_id' => $user_id,
'quota' => $quota,
'quota_max'=> $quota_max,
'message' => 'ok'
'success' => true,
'user_id' => $user_id,
'quota' => $quota,
'quota_max' => Statics::quota_max($is_pro),
'is_pro' => $is_pro,
'message' => 'ok'
]);
return 0;

View File

@ -6,6 +6,8 @@ class Statics
{
public static $DB = NULL;
public static $CFG = NULL;
public static function quota_max($is_pro) { return $is_pro ? 1000 : 100; }
}
function getConfig()
@ -15,6 +17,34 @@ function getConfig()
return Statics::$CFG = require "config.php";
}
function reportError($msg)
{
$subject = "SCN_Server has encountered an Error at " . date("Y-m-d H:i:s") . "] ";
$content = "";
$content .= 'HTTP_HOST: ' . ParamServerOrUndef('HTTP_HOST') . "\n";
$content .= 'REQUEST_URI: ' . ParamServerOrUndef('REQUEST_URI') . "\n";
$content .= 'TIME: ' . date('Y-m-d H:i:s') . "\n";
$content .= 'REMOTE_ADDR: ' . ParamServerOrUndef('REMOTE_ADDR') . "\n";
$content .= 'HTTP_X_FORWARDED_FOR: ' . ParamServerOrUndef('HTTP_X_FORWARDED_FOR') . "\n";
$content .= 'HTTP_USER_AGENT: ' . ParamServerOrUndef('HTTP_USER_AGENT') . "\n";
$content .= 'MESSAGE:' . "\n" . $msg . "\n";
$content .= '$_GET:' . "\n" . print_r($_GET, true) . "\n";
$content .= '$_POST:' . "\n" . print_r($_POST, true) . "\n";
$content .= '$_FILES:' . "\n" . print_r($_FILES, true) . "\n";
if (getConfig()['error_reporting']['send-mail'])sendMail($subject, $content, getConfig()['error_reporting']['email-error-target'], getConfig()['error_reporting']['email-error-sender']);
}
/**
* @param string $idx
* @return string
*/
function ParamServerOrUndef($idx) {
return isset($_SERVER[$idx]) ? $_SERVER[$idx] : 'NOT_SET';
}
function getDatabase()
{
if (Statics::$DB !== NULL) return Statics::$DB;
@ -57,6 +87,13 @@ function generateRandomAuthKey()
return $random;
}
/**
* @param $url
* @param $body
* @param $header
* @return array|object|string
* @throws \Httpful\Exception\ConnectionErrorException
*/
function sendPOST($url, $body, $header)
{
$builder = \Httpful\Request::post($url);
@ -71,3 +108,68 @@ function sendPOST($url, $body, $header)
return $response->body;
}
function verifyOrderToken($tok)
{
// https://developers.google.com/android-publisher/api-ref/purchases/products/get
try
{
$package = getConfig()['verify_api']['package_name'];
$product = getConfig()['verify_api']['product_id'];
$acctoken = getConfig()['verify_api']['accesstoken'];
if ($acctoken == '') $acctoken = refreshVerifyToken();
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
$json = sendPOST($url, "", []);
$obj = json_decode($json);
if ($obj === null || $obj === false)
{
reportError('verify-token returned NULL');
return false;
}
if (isset($obj['error']) && isset($obj['error']['code']) && $obj['error']['code'] == 401) // "Invalid Credentials" -- refresh acces_token
{
$acctoken = refreshVerifyToken();
$url = 'https://www.googleapis.com/androidpublisher/v3/applications/'.$package.'/purchases/products/'.$product.'/tokens/'.$tok.'?access_token='.$acctoken;
$json = sendPOST($url, "", []);
$obj = json_decode($json);
if ($obj === null || $obj === false)
{
reportError('verify-token returned NULL');
return false;
}
}
if (isset($obj['purchaseState']) && $obj['purchaseState'] === 0) return true;
return false;
}
catch (Exception $e)
{
reportError("VerifyOrder token threw exception: " . $e . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString());
return false;
}
}
/** @throws Exception */
function refreshVerifyToken()
{
$url = 'https://accounts.google.com/o/oauth2/token'.
'?grant_type=refresh_token'.
'&refresh_token='.getConfig()['verify_api']['refreshtoken'].
'&client_id='.getConfig()['verify_api']['clientid'].
'&client_secret='.getConfig()['verify_api']['clientsecret'];
$json = sendPOST($url, "", []);
$obj = json_decode($json);
file_put_contents('.verify_accesstoken', $obj['access_token']);
return $obj->access_token;
}

View File

@ -5,24 +5,34 @@ include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['fcm_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[fcm_token]]']));
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
$fcmtoken = $INPUT['fcm_token'];
$ispro = $INPUT['pro'] == 'true';
$pro_token = $INPUT['pro_token'];
$user_key = generateRandomAuthKey();
$pdo = getDatabase();
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, timestamp_accessed) VALUES (:key, :token, NOW())');
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken]);
if ($ispro)
{
if (!verifyOrderToken($pro_token)) die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
}
$stmt = $pdo->prepare('INSERT INTO users (user_key, fcm_token, is_pro, pro_token, timestamp_accessed) VALUES (:key, :token, :bpro, :spro, NOW())');
$stmt->execute(['key' => $user_key, 'token' => $fcmtoken, 'bpro' => $ispro, 'spro' => $ispro ? $pro_token : null]);
$user_id = $pdo->lastInsertId('user_id');
echo json_encode(
[
'success' => true,
'user_id' => $user_id,
'user_key' => $user_key,
'quota' => 0,
'quota_max'=> 100,
'message' => 'New user registered'
'success' => true,
'user_id' => $user_id,
'user_key' => $user_key,
'quota' => 0,
'quota_max' => Statics::quota_max($ispro),
'is_pro' => $ispro,
'message' => 'New user registered'
]);
return 0;

View File

@ -9,7 +9,9 @@ CREATE TABLE `users`
`quota_today` INT(11) NOT NULL DEFAULT '0',
`quota_day` DATE NULL DEFAULT NULL,
`quota_max` INT(11) NOT NULL DEFAULT '100',
`is_pro` BIT NOT NULL DEFAULT 0,
`pro_token` VARCHAR(256) NULL DEFAULT NULL,
PRIMARY KEY (`user_id`)
);

View File

@ -32,7 +32,7 @@ if (strlen($content) > 10000) die(json_encode(['success' => false, 'errhighli
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, fcm_token, messages_sent, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1');
$stmt = $pdo->prepare('SELECT user_id, user_key, fcm_token, messages_sent, quota_today, is_pro, quota_day FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
@ -47,7 +47,7 @@ $fcm = $data['fcm_token'];
$new_quota = $data['quota_today'] + 1;
if ($data['quota_day'] === null || $data['quota_day'] !== date("Y-m-d")) $new_quota=1;
if ($new_quota > $data['quota_max']) die(json_encode(['success' => false, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.$data['quota_max'].')']));
if ($new_quota > Statics::quota_max($data['is_pro'])) die(json_encode(['success' => false, 'errhighlight' => -1, 'message' => 'Daily quota reached ('.Statics::quota_max($data['is_pro']).')']));
//------------------------------------------------------------------
@ -90,11 +90,12 @@ $stmt->execute(['uid' => $user_id, 'q' => $new_quota]);
echo (json_encode(
[
'success' => true,
'message' => 'Message sent',
'response' => $httpresult,
'success' => true,
'message' => 'Message sent',
'response' => $httpresult,
'messagecount' => $data['messages_sent']+1,
'quota'=>$new_quota,
'quota_max'=>$data['quota_max'],
'quota' => $new_quota,
'is_pro' => $data['is_pro'],
'quota_max' => Statics::quota_max($data['is_pro']),
]));
return 0;

View File

@ -16,7 +16,7 @@ $fcm_token = isset($INPUT['fcm_token']) ? $INPUT['fcm_token'] : null;
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_max, quota_day FROM users WHERE user_id = :uid LIMIT 1');
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
@ -28,7 +28,7 @@ if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'me
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
$quota = $data['quota_today'];
$quota_max = $data['quota_max'];
$is_pro = $data['is_pro'];
$new_userkey = generateRandomAuthKey();
@ -39,12 +39,13 @@ if ($fcm_token === null)
echo json_encode(
[
'success' => true,
'user_id' => $user_id,
'success' => true,
'user_id' => $user_id,
'user_key' => $new_userkey,
'quota' => $quota,
'quota_max'=> $quota_max,
'message' => 'user updated'
'quota_max'=> Statics::quota_max($data['is_pro']),
'is_pro' => $is_pro,
'message' => 'user updated'
]);
return 0;
}
@ -59,7 +60,8 @@ else
'user_id' => $user_id,
'user_key' => $new_userkey,
'quota' => $quota,
'quota_max'=> $quota_max,
'quota_max'=> Statics::quota_max($data['is_pro']),
'is_pro' => $is_pro,
'message' => 'user updated'
]);
return 0;

78
web/upgrade.php Normal file
View File

@ -0,0 +1,78 @@
<?php
include_once 'model.php';
$INPUT = array_merge($_GET, $_POST);
if (!isset($INPUT['user_id'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_id]]']));
if (!isset($INPUT['user_key'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[user_key]]']));
if (!isset($INPUT['pro'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro]]']));
if (!isset($INPUT['pro_token'])) die(json_encode(['success' => false, 'message' => 'Missing parameter [[pro_token]]']));
$user_id = $INPUT['user_id'];
$user_key = $INPUT['user_key'];
$ispro = $INPUT['pro'] == 'true';
$pro_token = $INPUT['pro_token'];
//----------------------
$pdo = getDatabase();
$stmt = $pdo->prepare('SELECT user_id, user_key, quota_today, quota_day, is_pro, pro_token FROM users WHERE user_id = :uid LIMIT 1');
$stmt->execute(['uid' => $user_id]);
$datas = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (count($datas)<=0) die(json_encode(['success' => false, 'message' => 'User not found']));
$data = $datas[0];
if ($data === null) die(json_encode(['success' => false, 'message' => 'User not found']));
if ($data['user_id'] !== (int)$user_id) die(json_encode(['success' => false, 'message' => 'UserID not found']));
if ($data['user_key'] !== $user_key) die(json_encode(['success' => false, 'message' => 'Authentification failed']));
if ($ispro)
{
// set pro=true
if ($data['pro_token'] != $pro_token)
{
if (!verifyOrderToken($pro_token)) die(json_encode(['success' => false, 'message' => 'Purchase token could not be verified']));
}
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=1, pro_token=:ptk WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
$stmt = $pdo->prepare('UPDATE users SET is_pro=0, pro_token=NULL WHERE user_id <> :uid AND pro_token = :ptk');
$stmt->execute(['uid' => $user_id, 'ptk' => $pro_token]);
echo json_encode(
[
'success' => true,
'user_id' => $user_id,
'user_key' => $new_userkey,
'quota' => $data['quota'],
'quota_max'=> Statics::quota_max(true),
'is_pro' => true,
'message' => 'user updated'
]);
return 0;
}
else
{
// set pro=false
$stmt = $pdo->prepare('UPDATE users SET timestamp_accessed=NOW(), is_pro=0, pro_token=NULL WHERE user_id = :uid');
$stmt->execute(['uid' => $user_id]);
echo json_encode(
[
'success' => true,
'user_id' => $user_id,
'user_key' => $new_userkey,
'quota' => $data['quota'],
'quota_max'=> Statics::quota_max(false),
'is_pro' => false,
'message' => 'user updated'
]);
return 0;
}