Instant verification (#974)

* add instant verification options and Google Play Billing
bumpversion 0.15.16 --> 0.15.17
restore account_undergo_review default string
twitter sign-in flow
fix build error
final changes
update api key and secret
* tweak build script
This commit is contained in:
Akinwale Ariwodola 2020-08-19 17:23:35 +01:00 committed by GitHub
parent 4d024c06cc
commit 6931dbe79c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1042 additions and 72 deletions

1
.gitignore vendored
View file

@ -60,6 +60,7 @@ buck-out/
# Other Files
app/google-services.json
app/twitter.properties
*.log
.vagrant
*.hprof

View file

@ -11,7 +11,7 @@ build apk:
- apt-get -y update && apt-get -y install build-essential ca-certificates curl git gpg-agent openjdk-8-jdk software-properties-common wget zipalign
- chmod u+x $CI_PROJECT_DIR/gradlew
- export ANDROID_SDK_ROOT=~/.buildozer/android/platform/android-sdk-23
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1)
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
artifacts:
paths:
- bin/browser-*-release__arm.apk
@ -27,8 +27,8 @@ build apk:
- yarn
- chmod u+x ./release.sh
- ./release.sh
- cp bin/browser-$BUILD_VERSION-release__arm.apk /dev/null
- cp bin/browser-$BUILD_VERSION-release__arm64.apk /dev/null
- cp bin/browser-$BUILD_VERSION-release__arm.apk $CI_PROJECT_DIR
- cp bin/browser-$BUILD_VERSION-release__arm64.apk $CI_PROJECT_DIR
deploy build.lbry.io:
image: python:stretch
@ -39,7 +39,7 @@ deploy build.lbry.io:
- apt-get -y update && apt-get -y install openjdk-8-jdk
- pip install awscli
- chmod u+x $CI_PROJECT_DIR/gradlew
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1)
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
script:
@ -58,7 +58,7 @@ release apk:
- apt-get -y update && apt-get -y install openjdk-8-jdk
- pip install awscli githubrelease
- chmod u+x $CI_PROJECT_DIR/gradlew
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -q printVersionName --console=plain | tail -1)
- export BUILD_VERSION=$($CI_PROJECT_DIR/gradlew -p $CI_PROJECT_DIR -q printVersionName --console=plain | tail -1)
- export BUILD_APK_FILENAME__32=browser-$BUILD_VERSION-release__arm.apk
- export BUILD_APK_FILENAME__64=browser-$BUILD_VERSION-release__arm64.apk
script:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,2 +1,3 @@
lbry-android.keystore:0d958c531870694624cc877ea98ca1c583485f8ebbb3a5acca58b1930c190d65
app/google-services.json:896a0bee8294a36d061f10fa926129d8a780528b34d0a2f03113400c4246d67c
app/twitter.properties:01212d70712f2041efb5c814bf30ecbf6f72e1ca5179c7647c4f8cbd995dd033

View file

@ -1,5 +1,12 @@
import com.google.gms.googleservices.GoogleServicesPlugin
Properties twitterProps = new Properties()
try {
twitterProps.load(project.file('twitter.properties').newDataInputStream())
} catch (Exception ex) {
throw new GradleException("Missing twitter.properties.")
}
apply plugin: 'com.android.application'
android {
@ -22,6 +29,10 @@ android {
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
packagingOptions {
exclude 'META-INF/DEPENDENCIES'
}
productFlavors {
__32bit {
versionCode android.defaultConfig.versionCode * 10 + 1
@ -38,7 +49,13 @@ android {
}
buildTypes {
debug {
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
}
release {
resValue "string", "TWITTER_CONSUMER_KEY", "\"${twitterProps.getProperty("twitterConsumerKey")}\""
resValue "string", "TWITTER_CONSUMER_SECRET", "\"${twitterProps.getProperty("twitterConsumerSecret")}\""
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
@ -51,6 +68,13 @@ task printVersionName {
}
}
configurations {
all {
exclude module: 'httpclient'
exclude module: 'commons-logging'
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
@ -76,6 +100,9 @@ dependencies {
implementation 'com.google.firebase:firebase-analytics:17.4.2'
implementation 'com.google.android.gms:play-services-base:17.2.1'
implementation 'com.google.firebase:firebase-messaging:20.2.0'
implementation 'com.google.oauth-client:google-oauth-client:1.30.4'
implementation 'com.android.billingclient:billing:3.0.0'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.4'

Binary file not shown.

View file

@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.android.vending.BILLING" />
<application
android:allowBackup="true"

View file

@ -47,6 +47,13 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
@ -140,10 +147,13 @@ import io.lbry.browser.model.WalletBalance;
import io.lbry.browser.model.WalletSync;
import io.lbry.browser.model.lbryinc.LbryNotification;
import io.lbry.browser.model.lbryinc.Reward;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.model.lbryinc.Subscription;
import io.lbry.browser.tasks.GenericTaskHandler;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.tasks.claim.ClaimListResultHandler;
import io.lbry.browser.tasks.claim.ClaimListTask;
import io.lbry.browser.tasks.lbryinc.AndroidPurchaseTask;
import io.lbry.browser.tasks.lbryinc.ClaimRewardTask;
import io.lbry.browser.tasks.lbryinc.FetchRewardsTask;
import io.lbry.browser.tasks.LighthouseAutoCompleteTask;
@ -192,6 +202,7 @@ import okhttp3.OkHttpClient;
public class MainActivity extends AppCompatActivity implements SdkStatusListener {
static final String SKU_SKIP = "lbryskip";
private Map<String, Class> specialRouteFragmentClassMap;
@Getter
private boolean inPictureInPictureMode;
@ -213,7 +224,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
public static boolean startingPermissionRequest = false;
public static boolean startingSignInFlowActivity = false;
private BillingClient billingClient;
@Getter
private boolean enteringPIPMode = false;
private boolean fullSyncInProgress = false;
@ -408,6 +419,24 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// setup the billing client in main activity (to handle cases where the verification purchase flow may have been interrupted)
billingClient = BillingClient.newBuilder(this)
.setListener(new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null)
{
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
}
}
})
.enablePendingPurchases()
.build();
establishBillingClientConnection();
playerNotificationManager = new PlayerNotificationManager(
this, LbrynetService.NOTIFICATION_CHANNEL_ID, PLAYBACK_NOTIFICATION_ID, new PlayerNotificationDescriptionAdapter());
@ -1024,6 +1053,7 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
@Override
protected void onResume() {
super.onResume();
checkPurchases();
enteringPIPMode = false;
applyNavbarSigninPadding();
@ -1046,6 +1076,33 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
}*/
}
private void checkPurchases() {
if (billingClient != null) {
Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP);
if (result.getPurchasesList() != null) {
for (Purchase purchase : result.getPurchasesList()) {
handlePurchase(purchase);
}
}
}
}
private void handlePurchase(Purchase purchase) {
handleBillingPurchase(purchase, billingClient, MainActivity.this, null, new RewardVerifiedHandler() {
@Override
public void onSuccess(RewardVerified rewardVerified) {
if (Lbryio.currentUser != null) {
Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
}
}
@Override
public void onError(Exception error) {
// pass
}
});
}
private void checkPendingOpens() {
if (pendingFollowingReload) {
loadFollowingContent();
@ -2536,8 +2593,8 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
startupStages.put(STARTUP_STAGE_SUBSCRIPTIONS_RESOLVED, true);
}
} catch (Exception ex) {
// nope
android.util.Log.e(TAG, String.format("App startup failed: %s", ex.getMessage()), ex);
// nopecd
Log.e(TAG, String.format("App startup failed: %s", ex.getMessage()), ex);
return false;
} finally {
Helper.closeCloseable(reader);
@ -3269,6 +3326,53 @@ public class MainActivity extends AppCompatActivity implements SdkStatusListener
return (ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED);
}
private void establishBillingClientConnection() {
if (billingClient != null) {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// no need to do anything here. purchases are always checked server-side
checkPurchases();
}
}
@Override
public void onBillingServiceDisconnected() {
establishBillingClientConnection();
}
});
}
}
public static void handleBillingPurchase(
Purchase purchase,
BillingClient billingClient,
Context context,
View progressView,
RewardVerifiedHandler handler) {
String sku = purchase.getSku();
if (SKU_SKIP.equalsIgnoreCase(sku)) {
// send purchase token for verification
if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED
/*&& isSignatureValid(purchase)*/) {
// consume the purchase
String purchaseToken = purchase.getPurchaseToken();
ConsumeParams consumeParams = ConsumeParams.newBuilder().setPurchaseToken(purchaseToken).build();
billingClient.consumeAsync(consumeParams, new ConsumeResponseListener() {
@Override
public void onConsumeResponse(@NonNull BillingResult billingResult, @NonNull String s) {
}
});
// send the purchase token to the backend to complete verification
AndroidPurchaseTask task = new AndroidPurchaseTask(purchaseToken, progressView, context, handler);
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
}
public interface BackPressInterceptor {
boolean onBackPressed();
}

View file

@ -7,19 +7,35 @@ import android.content.IntentFilter;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.widget.ViewPager2;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
import com.google.android.material.snackbar.Snackbar;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import io.lbry.browser.adapter.VerificationPagerAdapter;
import io.lbry.browser.listener.SignInListener;
import io.lbry.browser.listener.WalletSyncListener;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.model.lbryinc.User;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.tasks.lbryinc.FetchCurrentUserTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.LbryAnalytics;
@ -32,11 +48,59 @@ public class VerificationActivity extends FragmentActivity implements SignInList
public static final int VERIFICATION_FLOW_REWARDS = 2;
public static final int VERIFICATION_FLOW_WALLET = 3;
private BillingClient billingClient;
private BroadcastReceiver sdkReceiver;
private String email;
private boolean signedIn;
private int flow;
private PurchasesUpdatedListener purchasesUpdatedListener = new PurchasesUpdatedListener() {
@Override
public void onPurchasesUpdated(@NonNull BillingResult billingResult, @Nullable List<Purchase> purchases) {
int responseCode = billingResult.getResponseCode();
if (responseCode == BillingClient.BillingResponseCode.OK && purchases != null)
{
for (Purchase purchase : purchases) {
if (MainActivity.SKU_SKIP.equalsIgnoreCase(purchase.getSku())) {
showLoading();
MainActivity.handleBillingPurchase(
purchase,
billingClient,
VerificationActivity.this, null, new RewardVerifiedHandler() {
@Override
public void onSuccess(RewardVerified rewardVerified) {
if (Lbryio.currentUser != null) {
Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
}
if (!rewardVerified.isRewardApproved()) {
// show pending purchase message (possible slow card tx)
Snackbar.make(findViewById(R.id.verification_pager), R.string.purchase_request_pending, Snackbar.LENGTH_LONG).show();
} else {
Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show();
}
setResult(RESULT_OK);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}
@Override
public void onError(Exception error) {
showFetchUserError(getString(R.string.purchase_request_failed_error));
hideLoading();
}
});
}
}
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -73,6 +137,12 @@ public class VerificationActivity extends FragmentActivity implements SignInList
};
registerReceiver(sdkReceiver, filter);
billingClient = BillingClient.newBuilder(this)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();
establishBillingClientConnection();
setContentView(R.layout.activity_verification);
ViewPager2 viewPager = findViewById(R.id.verification_pager);
viewPager.setUserInputEnabled(false);
@ -89,6 +159,24 @@ public class VerificationActivity extends FragmentActivity implements SignInList
});
}
private void establishBillingClientConnection() {
if (billingClient != null) {
billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(@NonNull BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) {
// no need to do anything here. purchases are always checked server-side
}
}
@Override
public void onBillingServiceDisconnected() {
establishBillingClientConnection();
}
});
}
}
public void onResume() {
super.onResume();
LbryAnalytics.setCurrentScreen(this, "Verification", "Verification");
@ -104,11 +192,13 @@ public class VerificationActivity extends FragmentActivity implements SignInList
flowHandled = true;
} else if (flow == VERIFICATION_FLOW_REWARDS) {
User user = Lbryio.currentUser;
if (!user.isIdentityVerified()) {
// disable phone verification for now
/*if (!user.isIdentityVerified()) {
// phone number verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
flowHandled = true;
} else if (!user.isRewardApproved()) {
} else */
if (!user.isRewardApproved()) {
// manual verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
flowHandled = true;
@ -195,10 +285,14 @@ public class VerificationActivity extends FragmentActivity implements SignInList
ViewPager2 viewPager = findViewById(R.id.verification_pager);
// for rewards, (show phone verification if not done, or manual verification if required)
if (flow == VERIFICATION_FLOW_REWARDS) {
if (!user.isIdentityVerified()) {
// skipping phone verification
/*if (!user.isIdentityVerified()) {
// phone number verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_PHONE, false);
} else if (!user.isRewardApproved()) {
} else
*/
if (!user.isRewardApproved()) {
// manual verification required
viewPager.setCurrentItem(VerificationPagerAdapter.PAGE_VERIFICATION_MANUAL, false);
} else {
@ -289,6 +383,54 @@ public class VerificationActivity extends FragmentActivity implements SignInList
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
}
@Override
public void onSkipQueueAction() {
if (billingClient != null) {
List<String> skuList = new ArrayList<>();
skuList.add(MainActivity.SKU_SKIP);
SkuDetailsParams detailsParams = SkuDetailsParams.newBuilder().
setType(BillingClient.SkuType.INAPP).
setSkusList(skuList).build();
billingClient.querySkuDetailsAsync(detailsParams, new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(@NonNull BillingResult billingResult, @Nullable List<SkuDetails> list) {
if (list != null && list.size() > 0) {
// we only queried one product, so it should be the first item in the list
SkuDetails skuDetails = list.get(0);
// launch the billing flow for skip queue
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder().
setSkuDetails(skuDetails).build();
billingClient.launchBillingFlow(VerificationActivity.this, billingFlowParams);
}
}
});
}
}
@Override
public void onTwitterVerified() {
Snackbar.make(findViewById(R.id.verification_pager), R.string.reward_verification_successful, Snackbar.LENGTH_LONG).show();
setResult(RESULT_OK);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
finish();
}
}, 3000);
}
@Override
public void onManualProgress(boolean progress) {
if (progress) {
findViewById(R.id.verification_close_button).setVisibility(View.GONE);
} else {
findViewById(R.id.verification_close_button).setVisibility(View.VISIBLE);
}
}
@Override
public void onDestroy() {
Helper.unregisterReceiver(sdkReceiver, this);

View file

@ -7,4 +7,7 @@ public interface SignInListener {
void onPhoneAdded(String countryCode, String phoneNumber);
void onPhoneVerified();
void onManualVerifyContinue();
void onSkipQueueAction();
void onTwitterVerified();
void onManualProgress(boolean progress);
}

View file

@ -0,0 +1,9 @@
package io.lbry.browser.model;
import lombok.Data;
@Data
public class TwitterOauth {
private String oauthToken;
private String oauthTokenSecret;
}

View file

@ -0,0 +1,9 @@
package io.lbry.browser.model.lbryinc;
import lombok.Data;
@Data
public class RewardVerified {
private long userId;
private boolean isRewardApproved;
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.tasks;
import io.lbry.browser.model.lbryinc.RewardVerified;
public interface RewardVerifiedHandler {
void onSuccess(RewardVerified rewardVerified);
void onError(Exception error);
}

View file

@ -0,0 +1,8 @@
package io.lbry.browser.tasks;
import io.lbry.browser.model.TwitterOauth;
public interface TwitterOauthHandler {
void onSuccess(TwitterOauth twitterOauth);
void onError(Exception error);
}

View file

@ -0,0 +1,68 @@
package io.lbry.browser.tasks.lbryinc;
import android.content.Context;
import android.os.AsyncTask;
import android.view.View;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.json.JSONObject;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.MainActivity;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
import okhttp3.Response;
public class AndroidPurchaseTask extends AsyncTask<Void, Void, RewardVerified> {
private Context context;
private View progressView;
private String purchaseToken;
private RewardVerifiedHandler handler;
private Exception error;
public AndroidPurchaseTask(String purchaseToken, View progressView, Context context, RewardVerifiedHandler handler) {
this.purchaseToken = purchaseToken;
this.progressView = progressView;
this.context = context;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected RewardVerified doInBackground(Void... params) {
try {
Map<String, String> options = new HashMap<>();
options.put("purchase_token", purchaseToken);
JSONObject object = (JSONObject) Lbryio.parseResponse(Lbryio.call("verification", "android_purchase", options, context));
Type type = new TypeToken<RewardVerified>(){}.getType();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
return gson.fromJson(object.toString(), type);
} catch (Exception ex) {
error = ex;
return null;
}
}
protected void onPostExecute(RewardVerified result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result != null) {
handler.onSuccess(result);
} else {
handler.onError(error);
}
}
}
}

View file

@ -0,0 +1,68 @@
package io.lbry.browser.tasks.lbryinc;
import android.content.Context;
import android.os.AsyncTask;
import android.view.View;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import org.json.JSONObject;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import io.lbry.browser.model.TwitterOauth;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
public class TwitterVerifyTask extends AsyncTask<Void, Void, RewardVerified> {
private Context context;
private View progressView;
private TwitterOauth twitterOauth;
private RewardVerifiedHandler handler;
private Exception error;
public TwitterVerifyTask(TwitterOauth twitterOauth, View progressView, Context context, RewardVerifiedHandler handler) {
this.twitterOauth = twitterOauth;
this.progressView = progressView;
this.context = context;
this.handler = handler;
}
protected void onPreExecute() {
Helper.setViewVisibility(progressView, View.VISIBLE);
}
protected RewardVerified doInBackground(Void... params) {
try {
Map<String, String> options = new HashMap<>();
options.put("oauth_token", twitterOauth.getOauthToken());
options.put("oauth_token_secret", twitterOauth.getOauthTokenSecret());
JSONObject object = (JSONObject) Lbryio.parseResponse(Lbryio.call("verification", "twitter_verify", options, context));
Type type = new TypeToken<RewardVerified>(){}.getType();
Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
return gson.fromJson(object.toString(), type);
} catch (Exception ex) {
error = ex;
return null;
}
}
protected void onPostExecute(RewardVerified result) {
Helper.setViewVisibility(progressView, View.GONE);
if (handler != null) {
if (result != null) {
handler.onSuccess(result);
} else {
handler.onError(error);
}
}
}
}

View file

@ -0,0 +1,68 @@
package io.lbry.browser.tasks.verification;
import android.os.AsyncTask;
import com.google.api.client.auth.oauth.OAuthHmacSigner;
import com.google.api.client.auth.oauth.OAuthParameters;
import com.google.api.client.http.GenericUrl;
import io.lbry.browser.VerificationActivity;
import io.lbry.browser.model.TwitterOauth;
import io.lbry.browser.tasks.TwitterOauthHandler;
import io.lbry.browser.utils.Helper;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class TwitterAccessTokenTask extends AsyncTask<Void, Void, String> {
private static final String ENDPOINT = "https://api.twitter.com/oauth/access_token";
private Exception error;
private String oauthParams;
private TwitterOauthHandler handler;
public TwitterAccessTokenTask(String oauthParams, TwitterOauthHandler handler) {
this.oauthParams = oauthParams;
this.handler = handler;
}
public String doInBackground(Void... params) {
try {
String url = String.format("%s?%s", ENDPOINT, oauthParams);
RequestBody body = RequestBody.create(new byte[0]);
Request request = new Request.Builder().url(url).post(body).build();
OkHttpClient client = new OkHttpClient.Builder().build();
Response response = client.newCall(request).execute();
return response.body().string();
} catch (Exception ex) {
error = ex;
return null;
}
}
protected void onPostExecute(String response) {
if (!Helper.isNullOrEmpty(response)) {
String[] pairs = response.split("&");
TwitterOauth twitterOauth = new TwitterOauth();
for (String pair : pairs) {
String[] parts = pair.split("=");
if (parts.length != 2) {
continue;
}
String key = parts[0];
String value = parts[1];
if ("oauth_token".equalsIgnoreCase(key)) {
twitterOauth.setOauthToken(value);
} else if ("oauth_token_secret".equalsIgnoreCase(key)) {
twitterOauth.setOauthTokenSecret(value);
}
}
handler.onSuccess(twitterOauth);
} else {
handler.onError(error);
}
}
}

View file

@ -0,0 +1,88 @@
package io.lbry.browser.tasks.verification;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Base64;
import com.google.api.client.auth.oauth.OAuthHmacSigner;
import com.google.api.client.auth.oauth.OAuthParameters;
import com.google.api.client.http.GenericUrl;
import java.nio.charset.StandardCharsets;
import io.lbry.browser.VerificationActivity;
import io.lbry.browser.model.TwitterOauth;
import io.lbry.browser.tasks.TwitterOauthHandler;
import io.lbry.browser.utils.Helper;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class TwitterRequestTokenTask extends AsyncTask<Void, Void, String> {
private static final String ENDPOINT = "https://api.twitter.com/oauth/request_token";
private String consumerKey;
private String consumerSecret;
private Exception error;
private TwitterOauthHandler handler;
public TwitterRequestTokenTask(String consumerKey, String consumerSecret, TwitterOauthHandler handler) {
this.consumerKey = consumerKey;
this.consumerSecret = consumerSecret;
this.handler = handler;
}
public String doInBackground(Void... params) {
try {
OAuthHmacSigner signer = new OAuthHmacSigner();
signer.clientSharedSecret = new String(
Base64.decode(consumerSecret, Base64.NO_WRAP), StandardCharsets.UTF_8.name());
OAuthParameters oauthParams = new OAuthParameters();
oauthParams.callback = "https://lbry.tv";
oauthParams.consumerKey = new String(
Base64.decode(consumerKey, Base64.NO_WRAP), StandardCharsets.UTF_8.name());;
oauthParams.signatureMethod = "HMAC-SHA-1";
oauthParams.signer = signer;
oauthParams.computeNonce();
oauthParams.computeTimestamp();
oauthParams.computeSignature("POST", new GenericUrl(ENDPOINT));
RequestBody body = RequestBody.create(new byte[0]);
Request request = new Request.Builder().url(ENDPOINT).addHeader(
"Authorization", oauthParams.getAuthorizationHeader()).post(body).build();
OkHttpClient client = new OkHttpClient.Builder().build();
Response response = client.newCall(request).execute();
return response.body().string();
} catch (Exception ex) {
error = ex;
return null;
}
}
protected void onPostExecute(String response) {
if (!Helper.isNullOrEmpty(response)) {
String[] pairs = response.split("&");
TwitterOauth twitterOauth = new TwitterOauth();
for (String pair : pairs) {
String[] parts = pair.split("=");
if (parts.length != 2) {
continue;
}
String key = parts[0];
String value = parts[1];
if ("oauth_token".equalsIgnoreCase(key)) {
twitterOauth.setOauthToken(value);
} else if ("oauth_token_secret".equalsIgnoreCase(key)) {
twitterOauth.setOauthTokenSecret(value);
}
}
handler.onSuccess(twitterOauth);
} else {
handler.onError(error);
}
}
}

View file

@ -1,27 +1,75 @@
package io.lbry.browser.ui.verification;
import android.content.Context;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.snackbar.Snackbar;
import io.lbry.browser.R;
import io.lbry.browser.listener.SignInListener;
import io.lbry.browser.model.TwitterOauth;
import io.lbry.browser.model.lbryinc.RewardVerified;
import io.lbry.browser.tasks.RewardVerifiedHandler;
import io.lbry.browser.tasks.TwitterOauthHandler;
import io.lbry.browser.tasks.lbryinc.TwitterVerifyTask;
import io.lbry.browser.tasks.verification.TwitterAccessTokenTask;
import io.lbry.browser.tasks.verification.TwitterRequestTokenTask;
import io.lbry.browser.utils.Helper;
import io.lbry.browser.utils.Lbryio;
import lombok.Setter;
public class ManualVerificationFragment extends Fragment {
@Setter
private SignInListener listener;
private PopupWindow popup;
private View mainView;
private View loadingView;
private TwitterOauth currentOauth;
private boolean twitterOauthInProgress = false;
private static final double SKIP_QUEUE_PRICE = 4.99;
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_verification_manual, container, false);
Helper.applyHtmlForTextView((TextView) root.findViewById(R.id.verification_manual_discord_verify));
mainView = root.findViewById(R.id.verification_manual_main);
loadingView = root.findViewById(R.id.verification_manual_loading);
Context context = getContext();
MaterialButton buttonSkipQueue = root.findViewById(R.id.verification_manual_skip_queue);
buttonSkipQueue.setText(context.getString(R.string.skip_queue_button_text, String.valueOf(SKIP_QUEUE_PRICE)));
Helper.applyHtmlForTextView(root.findViewById(R.id.verification_manual_discord_verify));
root.findViewById(R.id.verification_manual_twitter_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// start twitter verification
if (currentOauth != null) {
// Twitter three-legged oauth already completed, verify directly
twitterVerify(currentOauth);
} else {
// show twitter sign-in flow
twitterVerificationFlow();
}
}
});
root.findViewById(R.id.verification_manual_continue_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
@ -31,6 +79,205 @@ public class ManualVerificationFragment extends Fragment {
}
});
root.findViewById(R.id.verification_manual_skip_queue).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (listener != null) {
listener.onSkipQueueAction();
}
}
});
return root;
}
private void twitterVerificationFlow() {
twitterOauthInProgress = true;
if (listener != null) {
listener.onManualProgress(twitterOauthInProgress);
}
showLoading();
String consumerKey = getResources().getString(R.string.TWITTER_CONSUMER_KEY);
String consumerSecret = getResources().getString(R.string.TWITTER_CONSUMER_SECRET);
TwitterRequestTokenTask task = new TwitterRequestTokenTask(consumerKey, consumerSecret, new TwitterOauthHandler() {
@Override
public void onSuccess(TwitterOauth twitterOauth) {
twitterOauthInProgress = false;
if (listener != null) {
listener.onManualProgress(twitterOauthInProgress);
}
showTwitterAuthenticateWithToken(twitterOauth.getOauthToken());
}
@Override
public void onError(Exception error) {
handleFlowError(null);
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
public void showLoading() {
Helper.setViewVisibility(mainView, View.INVISIBLE);
Helper.setViewVisibility(loadingView, View.VISIBLE);
}
public void hideLoading() {
Helper.setViewVisibility(mainView, View.VISIBLE);
Helper.setViewVisibility(loadingView, View.GONE);
}
private void showTwitterAuthenticateWithToken(String oauthToken) {
Context context = getContext();
if (context != null) {
WebView webView = new WebView(context);
webView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT));
webView.loadUrl(String.format("https://api.twitter.com/oauth/authorize?oauth_token=%s", oauthToken));
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.startsWith("https://lbry.tv") || url.equalsIgnoreCase("https://twitter.com/home") /* Return to Twitter */) {
if (url.startsWith("https://lbry.tv") && url.contains("oauth_token") && url.contains("oauth_verifier")) {
// finish 3-legged oauth
twitterOauthInProgress = true;
listener.onManualProgress(twitterOauthInProgress);
finishTwitterOauth(url);
}
if (popup != null) {
popup.dismiss();
}
return false;
}
view.loadUrl(url);
return true;
}
});
View popupView = LayoutInflater.from(context).inflate(R.layout.popup_webview, null);
((LinearLayout) popupView.findViewById(R.id.popup_webivew_container)).addView(webView);
popupView.findViewById(R.id.popup_cancel_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (!twitterOauthInProgress && popup != null) {
popup.dismiss();
hideLoading();
}
}
});
float scale = getResources().getDisplayMetrics().density;
popup = new PopupWindow(context);
popup.setOnDismissListener(new PopupWindow.OnDismissListener() {
@Override
public void onDismiss() {
if (!twitterOauthInProgress) {
hideLoading();
}
popup = null;
}
});
popup.setWidth(Helper.getScaledValue(340, scale));
popup.setHeight(Helper.getScaledValue(480, scale));
popup.setContentView(popupView);
View parent = getView();
popup.setFocusable(true);
popup.showAtLocation(parent, Gravity.CENTER, 0, 0);
popup.update();
}
}
private void finishTwitterOauth(String callbackUrl) {
String params = callbackUrl.substring(callbackUrl.indexOf('?') + 1);
TwitterAccessTokenTask task = new TwitterAccessTokenTask(params, new TwitterOauthHandler() {
@Override
public void onSuccess(TwitterOauth twitterOauth) {
// send request to finish verifying
currentOauth = twitterOauth;
twitterVerify(twitterOauth);
}
@Override
public void onError(Exception error) {
handleFlowError(null);
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void twitterVerify(TwitterOauth twitterOauth) {
Context context = getContext();
if (context != null) {
showLoading();
twitterOauthInProgress = true;
if (listener != null) {
listener.onManualProgress(twitterOauthInProgress);
}
TwitterVerifyTask task = new TwitterVerifyTask(twitterOauth, null, context, new RewardVerifiedHandler() {
@Override
public void onSuccess(RewardVerified rewardVerified) {
twitterOauthInProgress = false;
if (listener != null) {
listener.onManualProgress(twitterOauthInProgress);
}
if (Lbryio.currentUser != null) {
Lbryio.currentUser.setRewardApproved(rewardVerified.isRewardApproved());
}
if (rewardVerified.isRewardApproved()) {
if (listener != null) {
listener.onTwitterVerified();
}
} else {
View root = getView();
if (root != null) {
// reward approved wasn't set to true
Snackbar.make(root, getString(R.string.twitter_verification_not_approved), Snackbar.LENGTH_LONG).
setTextColor(Color.WHITE).
setBackgroundTint(Color.RED).show();
}
hideLoading();
}
}
@Override
public void onError(Exception error) {
handleFlowError(error != null ? error.getMessage() : null);
}
});
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} else {
twitterOauthInProgress = false;
if (listener != null) {
listener.onManualProgress(twitterOauthInProgress);
}
hideLoading();
}
}
private void handleFlowError(String extra) {
hideLoading();
twitterOauthInProgress = false;
if (listener != null) {
listener.onManualProgress(twitterOauthInProgress);
}
showFlowError(extra);
}
private void showFlowError(String extra) {
Context context = getContext();
View root = getView();
if (context != null && root != null) {
String message = !Helper.isNullOrEmpty(extra) ?
getString(R.string.twitter_account_ineligible, extra) :
getString(R.string.twitter_verification_failed);
Snackbar.make(root, message, Snackbar.LENGTH_LONG).
setTextColor(Color.WHITE).
setBackgroundTint(Color.RED).show();
}
}
}

View file

@ -1,5 +1,7 @@
package io.lbry.browser.utils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@ -128,7 +130,7 @@ public final class Lbry {
IS_STATUS_PARSED = true;
} catch (JSONException | LbryResponseException ex) {
// pass
android.util.Log.e(TAG, "Could not parse status response.", ex);
Log.e(TAG, "Could not parse status response.", ex);
}
}

View file

@ -101,6 +101,7 @@ public final class Lbryio {
}
url = uriBuilder.build().toString();
}
/*if (BuildConfig.DEBUG) {
Log.d(TAG, String.format("Request Method: %s, Sending request to URL: %s", method, url));
}*/
@ -200,6 +201,7 @@ public final class Lbryio {
throw new LbryioResponseException("Unknown API error signature.", response.code());
}
} catch (JSONException | IOException ex) {
throw new LbryioResponseException(String.format("Could not parse response: %s", responseString), ex);
}
}
@ -230,7 +232,7 @@ public final class Lbryio {
}
}
android.util.Log.e(TAG, "Could not retrieve the current user", ex);
Log.e(TAG, "Could not retrieve the current user", ex);
return null;
}
}
@ -308,7 +310,7 @@ public final class Lbryio {
context.sendBroadcast(intent);
}
} catch (Exception ex) {
android.util.Log.e(TAG, "Error sending encrypted auth token action broadcast", ex);
Log.e(TAG, "Error sending encrypted auth token action broadcast", ex);
// pass
}
}

View file

@ -11,6 +11,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/verification_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<RelativeLayout
android:id="@+id/verification_close_button"
android:visibility="gone"
@ -29,10 +34,6 @@
android:tint="@color/white" />
</RelativeLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/verification_pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ProgressBar
android:id="@+id/verification_loading_progress"
android:visibility="gone"

View file

@ -4,6 +4,18 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/verification_manual_loading"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_centerInParent="true"
android:visibility="gone" />
<androidx.core.widget.NestedScrollView
android:id="@+id/verification_manual_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
@ -11,13 +23,12 @@
android:layout_centerVertical="true"
android:visibility="visible"
android:layout_margin="36dp">
<TextView
android:textSize="28sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="@font/inter"
android:text="@string/manual_reward_verification"
android:text="@string/reward_verification"
android:textColor="@color/white"
android:textFontWeight="300" />
<TextView
@ -26,7 +37,71 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/inter"
android:text="@string/account_undergo_review"
android:text="@string/get_instantly_verified"
android:textColor="@color/white"
android:textFontWeight="300" />
<TextView
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:fontFamily="@font/inter"
android:text="@string/twitter_verification"
android:textColor="@color/white"
android:textFontWeight="300" />
<TextView
android:textSize="16sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/inter"
android:text="@string/twitter_verification_desc"
android:textColor="@color/white"
android:textFontWeight="300" />
<com.google.android.material.button.MaterialButton
android:id="@+id/verification_manual_twitter_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/inter"
android:text="@string/twitter_verify" />
<TextView
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:fontFamily="@font/inter"
android:text="@string/skip_queue_verification"
android:textColor="@color/white"
android:textFontWeight="300" />
<TextView
android:textSize="16sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/inter"
android:text="@string/skip_queue_verification_desc"
android:textColor="@color/white"
android:textFontWeight="300" />
<com.google.android.material.button.MaterialButton
android:id="@+id/verification_manual_skip_queue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:fontFamily="@font/inter"
android:text="@string/skip_queue_button_text" />
<TextView
android:textSize="20sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="36dp"
android:fontFamily="@font/inter"
android:text="@string/manual_reward_verification"
android:textColor="@color/white"
android:textFontWeight="300" />
<TextView
@ -34,11 +109,12 @@
android:textSize="16sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/inter"
android:text="@string/request_to_be_verified"
android:textColor="@color/white"
android:textFontWeight="300" />
<TextView
android:textSize="16sp"
android:layout_width="match_parent"
@ -53,8 +129,9 @@
android:id="@+id/verification_manual_continue_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="4dp"
android:fontFamily="@font/inter"
android:text="@string/continue_text" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</RelativeLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="340dp"
android:layout_height="480dp">
<LinearLayout
android:id="@+id/popup_webivew_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/popup_cancel_button"
android:orientation="vertical" />
<com.google.android.material.button.MaterialButton
android:id="@+id/popup_cancel_button"
android:text="@string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
style="@style/Widget.MaterialComponents.Button.TextButton"/>
</RelativeLayout>

View file

@ -422,8 +422,18 @@
<string name="enter_phone_number">Please enter your phone number.</string>
<string name="not_interested">Not interested</string>
<string name="manual_reward_verification">Manual Reward Verification</string>
<string name="reward_verification">Reward Verification</string>
<string name="social_media_verification">Social Media Verification</string>
<string name="skip_the_queue">Skip the Queue</string>
<string name="skip_queue_button_text">Skip for $%1$s</string>
<string name="account_undergo_review">This account must undergo review before you can participate in the rewards program. This can take anywhere from several minutes to several days.</string>
<string name="request_to_be_verified">If you continue to see this message, please request to be verified on the &lt;a href="https://discordapp.com/invite/Z3bERWA"&gt;LBRY Discord server&lt;/a&gt;.</string>
<string name="get_instantly_verified">You can get instantly verified to be able to participate in the rewards program using your Twitter account or skipping the manual verification queue.</string>
<string name="twitter_verification">Twitter Verification</string>
<string name="twitter_verification_desc">Get instantly verified using your Twitter account. Your Twitter email address must match the email that you provided and your account should be active.</string>
<string name="twitter_verify">Verify with Twitter</string>
<string name="skip_queue_verification">Skip the Queue</string>
<string name="skip_queue_verification_desc">Skip the manual verification queue by paying a fee in order to start participating in the rewards program immediately.</string>
<string name="request_to_be_verified">Please request to be verified on the &lt;a href="https://discordapp.com/invite/Z3bERWA"&gt;LBRY Discord server&lt;/a&gt;. A manual review can take anywhere from several minutes to several days.</string>
<string name="enjoy_free_content">Please enjoy free content in the meantime!</string>
<string name="verify_phone_number">Verify Phone Number</string>
<string name="enter_phone_verify_code">Please enter the verification code sent to %1$s</string>
@ -432,6 +442,12 @@
<string name="please_enter_valid_phone">Please enter a valid phone number.</string>
<string name="please_enter_verification_code">Please enter the verification code sent to your phone number.</string>
<string name="fetch_current_user_error">User account could not be retrieved at this time. Please try again later.</string>
<string name="purchase_request_pending">Your purchase request is still being processed. Please send an email to support@lbry.com.</string>
<string name="purchase_request_failed_error">Your purchase request could not be completed at this time. Please send an email to support@lbry.com.</string>
<string name="twitter_account_ineligible">Your Twitter account is not eligible at this time: %1$s</string>
<string name="twitter_verification_not_approved">Your account was not approved for the rewards program. Please try again later.</string>
<string name="twitter_verification_failed">Twitter verification failed. Please try again later.</string>
<string name="reward_verification_successful">You are now eligible to participate in the rewards program!</string>
<!-- Forms -->
<string name="no_added_tags">You have not added any tags yet. Add tags to improve discovery.</string>

Binary file not shown.

Binary file not shown.