diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt
index 921c1b84b270999e5ad8b413b58a9672afa58201..a0c0061fc6d95cf5a45b6d623c85de3cb742172d 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/ui/MailboxViewModel.kt
@@ -80,9 +80,7 @@ class MailboxViewModel @Inject constructor(
     ) { ls, ts, sc ->
         when {
             ls != LifecycleState.RUNNING -> Starting(ls.name)
-            // TODO waiting for ACTIVE is better than not doing it but to fix #90 we need to listen for
-            //  upload events to the hsdirs
-            ts != TorPlugin.State.ACTIVE -> Starting(ts.name + " TOR")
+            ts != TorPlugin.State.PUBLISHED -> Starting(ts.name + " TOR")
             sc == SetupComplete.FALSE -> {
                 val dm = Resources.getSystem().displayMetrics
                 val size = min(dm.widthPixels, dm.heightPixels)
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
index 1c0c0514fdc0510a6a566e491bbd8543e69f47e2..f0028e4912897586cd93aeba5dda3f0e96b45bb2 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
@@ -150,7 +150,7 @@ class Main : CliktCommand(
             runBlocking {
                 // wait until Tor becomes active and published the onion service
                 torPlugin.state.takeWhile { state ->
-                    state != TorPlugin.State.ACTIVE
+                    state != TorPlugin.State.PUBLISHED
                 }
             }
             qrCodeEncoder.getQrCodeBitMatrix()?.let {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
index 719e137a074c4fa090316703cfbe7dc690daf6d9..e3a41680516d096129a47153dc68607bf8b68595 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/setup/QrCodeEncoder.kt
@@ -73,7 +73,6 @@ class QrCodeEncoder @Inject constructor(
             LOG.error("Hidden service address not yet available")
             return null
         }
-        LOG.error(addressString)
         val addressBytes = Base32.decode(addressString.uppercase())
         check(addressBytes.size == 35) { "$addressString not 35 bytes long" }
         return addressBytes.copyOfRange(0, 32)
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
index 489f57d7262badf944729d9d09873aa6d08f486e..89a4e69fcc8cc63e277d9bbc23ed076f8b21cbdc 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
@@ -76,6 +76,7 @@ import static org.briarproject.mailbox.core.tor.TorConstants.SETTINGS_NAMESPACE;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.ACTIVE;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.ENABLING;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.INACTIVE;
+import static org.briarproject.mailbox.core.tor.TorPlugin.State.PUBLISHED;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.STARTING_STOPPING;
 import static org.briarproject.mailbox.core.util.IoUtils.copyAndClose;
 import static org.briarproject.mailbox.core.util.IoUtils.tryToClose;
@@ -367,11 +368,11 @@ public abstract class TorPlugin
 			s = new Settings();
 		}
 		String privateKey3 = s.get(HS_PRIVATE_KEY_V3);
-		publishV3HiddenService(port, privateKey3);
+		createV3HiddenService(port, privateKey3);
 	}
 
 	@IoExecutor
-	private void publishV3HiddenService(String port, @Nullable String privKey) {
+	private void createV3HiddenService(String port, @Nullable String privKey) {
 		LOG.info("Creating v3 hidden service");
 		Map<Integer, String> portLines = singletonMap(80, "127.0.0.1:" + port);
 		Map<String, String> response;
@@ -400,9 +401,6 @@ public abstract class TorPlugin
 		s.put(HS_ADDRESS_V3, onion3);
 		info(LOG, () -> "V3 hidden service " + scrubOnion(onion3));
 
-		// TODO remove before release
-		LOG.warn("V3 hidden service: http://" + onion3 + ".onion");
-
 		if (privKey == null) {
 			s.put(HS_PRIVATE_KEY_V3, response.get(HS_PRIVKEY));
 			try {
@@ -411,7 +409,6 @@ public abstract class TorPlugin
 				logException(LOG, e, "Error while merging settings");
 			}
 		}
-		state.setServicePublished();
 	}
 
 	@Nullable
@@ -505,6 +502,7 @@ public abstract class TorPlugin
 	public void unrecognized(String type, String msg) {
 		if (type.equals("HS_DESC") && msg.startsWith("UPLOADED")) {
 			LOG.info("V3 descriptor uploaded");
+			state.onServiceDescriptorUploaded();
 		}
 	}
 
@@ -599,8 +597,9 @@ public abstract class TorPlugin
 				networkInitialised = false,
 				networkEnabled = false,
 				bootstrapped = false,
-				circuitBuilt = false,
-				servicePublished = false;
+				circuitBuilt = false;
+		@GuardedBy("this")
+		private int numServiceUploads = 0;
 
 		@GuardedBy("this")
 		@Nullable
@@ -636,8 +635,8 @@ public abstract class TorPlugin
 			return firstCircuit;
 		}
 
-		synchronized void setServicePublished() {
-			servicePublished = true;
+		synchronized void onServiceDescriptorUploaded() {
+			numServiceUploads++;
 			state.setValue(getCurrentState());
 		}
 
@@ -654,8 +653,9 @@ public abstract class TorPlugin
 			}
 			if (!networkInitialised) return ENABLING;
 			if (!networkEnabled) return INACTIVE;
-			return bootstrapped && circuitBuilt && servicePublished ?
-					ACTIVE : ENABLING;
+			if (bootstrapped && circuitBuilt) {
+				return (numServiceUploads >= 3) ? PUBLISHED : ACTIVE;
+			} else return ENABLING;
 		}
 
 	}
@@ -677,6 +677,11 @@ public abstract class TorPlugin
 		 */
 		ACTIVE,
 
+		/**
+		 * The plugin has published the onion service.
+		 */
+		PUBLISHED,
+
 		/**
 		 * The plugin is enabled but can't make or receive connections
 		 */