Inverted flow happy path

Stephen Paul Weber created

When doing first-time signup, create an account on the onboarding server and
jump into register command at cheogram.  When that command is done, we expect it
will give us an OOB with xmmp:domain.tld?register which will jump us into magic
create flow for that server.  Once the new account is setup, if all we
previously had was an onboarding account we execute jidSwitch and delete the
onboarding account.

If anything goes wrong you are left with the onboarding account still tied to
cheogram and set up in the app as a "normal" account.

If the registration finishes without jumping you to a ?register URI you are left
with the onboarding account, but if you make an account then it will do the
switch after that (it tries the switch blindly, without checking if the current
backend supports it).

If you press "no thanks" you just get a spinner forever.

Change summary

src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java         | 42 
src/main/java/eu/siacs/conversations/Config.java                         |  1 
src/main/java/eu/siacs/conversations/entities/Conversation.java          |  8 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  4 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        |  3 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java       |  4 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java   | 92 
7 files changed, 141 insertions(+), 13 deletions(-)

Detailed changes

src/cheogram/java/eu/siacs/conversations/ui/WelcomeActivity.java 🔗

@@ -17,15 +17,18 @@ import androidx.annotation.NonNull;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.databinding.DataBindingUtil;
 
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.List;
 import java.util.HashSet;
+import java.util.UUID;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityWelcomeBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.Compatibility;
 import eu.siacs.conversations.utils.InstallReferrerUtils;
 import eu.siacs.conversations.utils.SignupUtils;
@@ -35,11 +38,12 @@ import eu.siacs.conversations.xmpp.Jid;
 import static eu.siacs.conversations.utils.PermissionUtils.allGranted;
 import static eu.siacs.conversations.utils.PermissionUtils.writeGranted;
 
-public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, KeyChainAliasCallback {
+public class WelcomeActivity extends XmppActivity implements XmppConnectionService.OnAccountCreated, XmppConnectionService.OnAccountUpdate, KeyChainAliasCallback {
 
     private static final int REQUEST_IMPORT_BACKUP = 0x63fb;
 
     private XmppUri inviteUri;
+    private Account onboardingAccount = null;
 
     public static void launch(AppCompatActivity activity) {
         Intent intent = new Intent(activity, WelcomeActivity.class);
@@ -82,8 +86,21 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
     }
 
     @Override
-    protected void refreshUiReal() {
+    protected synchronized void refreshUiReal() {
+        if (onboardingAccount == null) return;
+        if (onboardingAccount.getStatus() != Account.State.ONLINE) return;
 
+        Intent intent = new Intent(this, StartConversationActivity.class);
+        intent.putExtra("init", true);
+        intent.putExtra(EXTRA_ACCOUNT, onboardingAccount.getJid().asBareJid().toEscapedString());
+        onboardingAccount = null;
+        startActivity(intent);
+        finish();
+    }
+
+    @Override
+    public void onAccountUpdate() {
+        refreshUi();
     }
 
     @Override
@@ -124,9 +141,18 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
         setSupportActionBar(binding.toolbar);
         configureActionBar(getSupportActionBar(), false);
         binding.registerNewAccount.setOnClickListener(v -> {
-            final Intent intent = new Intent(this, MagicCreateActivity.class);
-            addInviteUri(intent);
-            startActivity(intent);
+            if (hasInviteUri()) {
+                final Intent intent = new Intent(this, MagicCreateActivity.class);
+                addInviteUri(intent);
+                startActivity(intent);
+            } else {
+                binding.registerNewAccount.setText("Working...");
+                binding.registerNewAccount.setEnabled(false);
+                onboardingAccount = new Account(Jid.ofLocalAndDomain(UUID.randomUUID().toString(), Config.ONBOARDING_DOMAIN.toEscapedString()), CryptoHelper.createPassword(new SecureRandom()));
+                onboardingAccount.setOption(Account.OPTION_REGISTER, true);
+                onboardingAccount.setOption(Account.OPTION_FIXED_USERNAME, true);
+                xmppConnectionService.createAccount(onboardingAccount);
+            }
         });
         binding.useExisting.setOnClickListener(v -> {
             final List<Account> accounts = xmppConnectionService.getAccounts();
@@ -235,6 +261,12 @@ public class WelcomeActivity extends XmppActivity implements XmppConnectionServi
         }
     }
 
+    protected boolean hasInviteUri() {
+        final Intent from = getIntent();
+        if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) return true;
+        return this.inviteUri != null;
+    }
+
     public void addInviteUri(Intent to) {
         final Intent from = getIntent();
         if (from != null && from.hasExtra(StartConversationActivity.EXTRA_INVITE_URI)) {

src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -48,6 +48,7 @@ public final class Config {
     public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
     public static final String MAGIC_CREATE_DOMAIN = "chatterboxtown.us";
     public static final Jid QUICKSY_DOMAIN = Jid.of("cheogram.com");
+    public static final Jid ONBOARDING_DOMAIN = Jid.of("onboarding.cheogram.com");
 
     public static final String CHANNEL_DISCOVERY = "https://search.jabber.network";
 

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -797,7 +797,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 }
             }
         } else if ((QuickConversationsService.isConversations() || !Config.QUICKSY_DOMAIN.equals(contactJid.getDomain())) && isWithStranger()) {
-            return contactJid;
+            return contactJid.equals(Jid.of("cheogram.com")) ? "Cheogram" : contactJid;
         } else {
             return this.getContact().getDisplayName();
         }
@@ -2400,7 +2400,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             }
 
             public void updateWithResponse(final IqPacket iq) {
-                if (getView().isAttachedToWindow()) {
+                if (getView() != null && getView().isAttachedToWindow()) {
                     getView().post(() -> updateWithResponseUiThread(iq));
                 } else {
                     pendingResponsePacket = iq;
@@ -2706,6 +2706,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             }
 
             public View getView() {
+                if (mBinding == null) return null;
                 return mBinding.getRoot();
             }
 
@@ -2874,8 +2875,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                 actionsAdapter.notifyDataSetChanged();
 
                 if (pendingResponsePacket != null) {
-                    updateWithResponseUiThread(pendingResponsePacket);
+                    final IqPacket pending = pendingResponsePacket;
                     pendingResponsePacket = null;
+                    updateWithResponseUiThread(pending);
                 }
             }
 

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -1805,6 +1805,10 @@ public class XmppConnectionService extends Service {
         sendMessage(message, true, delay);
     }
 
+    public boolean isOnboarding() {
+        return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
+    }
+
     public void requestEasyOnboardingInvite(final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
         final XmppConnection connection = account.getXmppConnection();
         final Jid jid = connection == null ? null : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -1202,6 +1202,8 @@ public class ConversationFragment extends XmppFragment
 
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
+        if (activity.xmppConnectionService.isOnboarding()) return;
+
         menuInflater.inflate(R.menu.fragment_conversation, menu);
         final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
         final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details);
@@ -2997,6 +2999,7 @@ public class ConversationFragment extends XmppFragment
                 }
                 if (node != null && commandJid != null) {
                     conversation.startCommand(commandFor(commandJid, node), activity.xmppConnectionService);
+                    if (activity.xmppConnectionService.isOnboarding()) binding.tabLayout.setVisibility(View.GONE);
                 }
             });
             return;

src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java 🔗

@@ -740,10 +740,10 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
             final Conversation conversation = ((ConversationFragment) mainFragment).getConversation();
             if (conversation != null) {
                 actionBar.setTitle(conversation.getName());
-                actionBar.setDisplayHomeAsUpEnabled(true);
+                actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding());
                 ActionBarUtil.setActionBarOnClickListener(
                         binding.toolbar,
-                        (v) -> openConversationDetails(conversation)
+                        (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); }
                 );
                 return;
             }

src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java 🔗

@@ -95,9 +95,13 @@ import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Namespace;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import eu.siacs.conversations.xmpp.XmppConnection;
+import eu.siacs.conversations.xmpp.forms.Data;
+import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 
 public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
 
@@ -918,8 +922,11 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             final String accountJid = intent.getStringExtra(EXTRA_ACCOUNT);
             intent = null;
             boolean hasPstnOrSms = false;
+            Account onboardingAccount = null;
             outer:
             for (Account account : xmppConnectionService.getAccounts()) {
+                if (onboardingAccount == null && account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) onboardingAccount = account;
+
                 if (accountJid != null) {
                     if(account.getJid().asBareJid().toEscapedString().equals(accountJid)) {
                         selectedAccount = account;
@@ -941,9 +948,88 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             }
 
             if (!hasPstnOrSms) {
-                startCommand(selectedAccount, Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register");
-                finish();
-                return;
+                if (onboardingAccount != null && !selectedAccount.getJid().equals(onboardingAccount.getJid())) {
+                    final Account onboardAccount = onboardingAccount;
+                    final Account newAccount = selectedAccount;
+                    final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
+                    packet.setTo(Jid.of("cheogram.com"));
+                    final Element c = packet.addChild("command", Namespace.COMMANDS);
+                    c.setAttribute("node", "change jabber id");
+                    c.setAttribute("action", "execute");
+
+                    xmppConnectionService.sendIqPacket(onboardingAccount, packet, (a, iq) -> {
+                        Element command = iq.findChild("command", "http://jabber.org/protocol/commands");
+                        if (command == null) {
+                            Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq);
+                            return;
+                        }
+
+                        Element form = command.findChild("x", "jabber:x:data");
+                        Data dataForm = form == null ? null : Data.parse(form);
+                        if (dataForm == null || dataForm.getFieldByName("new-jid") == null) {
+                            Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq);
+                            return;
+                        }
+
+                        dataForm.put("new-jid", newAccount.getJid().toEscapedString());
+                        dataForm.submit();
+                        command.setAttribute("action", "execute");
+                        iq.setTo(iq.getFrom());
+                        iq.setAttribute("type", "set");
+                        iq.removeAttribute("from");
+                        iq.removeAttribute("id");
+                        xmppConnectionService.sendIqPacket(a, iq, (a2, iq2) -> {
+                            Element command2 = iq2.findChild("command", "http://jabber.org/protocol/commands");
+                            if (command2 != null && command2.getAttribute("status") != null && command2.getAttribute("status").equals("completed")) {
+                                final IqPacket regPacket = new IqPacket(IqPacket.TYPE.SET);
+                                regPacket.setTo(Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"));
+                                final Element c2 = regPacket.addChild("command", Namespace.COMMANDS);
+                                c2.setAttribute("node", "jabber:iq:register");
+                                c2.setAttribute("action", "execute");
+                                xmppConnectionService.sendIqPacket(newAccount, regPacket, (a3, iq3) -> {
+                                    Element command3 = iq3.findChild("command", "http://jabber.org/protocol/commands");
+                                    if (command3 == null) {
+                                        Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3);
+                                        return;
+                                    }
+
+                                    Element form3 = command3.findChild("x", "jabber:x:data");
+                                    Data dataForm3 = form3 == null ? null : Data.parse(form3);
+                                    if (dataForm3 == null || dataForm3.getFieldByName("confirm") == null) {
+                                        Log.e(Config.LOGTAG, "Did not get expected data form from cheogram, got: " + iq3);
+                                        return;
+                                    }
+
+                                    dataForm3.put("confirm", "true");
+                                    dataForm3.submit();
+                                    command3.setAttribute("action", "execute");
+                                    iq3.setTo(iq3.getFrom());
+                                    iq3.setAttribute("type", "set");
+                                    iq3.removeAttribute("from");
+                                    iq3.removeAttribute("id");
+                                    xmppConnectionService.sendIqPacket(newAccount, iq3, (a4, iq4) -> {
+                                        Element command4 = iq2.findChild("command", "http://jabber.org/protocol/commands");
+                                        if (command4 != null && command4.getAttribute("status") != null && command4.getAttribute("status").equals("completed")) {
+                                            xmppConnectionService.createContact(newAccount.getRoster().getContact(iq4.getFrom().asBareJid()), true);
+                                            Conversation withCheogram = xmppConnectionService.findOrCreateConversation(newAccount, iq4.getFrom().asBareJid(), true, true, true);
+                                            xmppConnectionService.markRead(withCheogram);
+                                            xmppConnectionService.clearConversationHistory(withCheogram);
+                                            xmppConnectionService.deleteAccount(onboardAccount);
+                                        } else {
+                                            Log.e(Config.LOGTAG, "Error confirming jid switch, got: " + iq4);
+                                        }
+                                    });
+                                });
+                            } else {
+                                Log.e(Config.LOGTAG, "Error during jid switch, got: " + iq2);
+                            }
+                        });
+                    });
+                } else {
+                    startCommand(selectedAccount, Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register");
+                    finish();
+                    return;
+                }
             }
         }