Page MenuHomePhabricator

Add Special:MobileAppRedirect to MobileApp extension for app store redirection
Open, LowPublic8 Estimated Story Points

Description

Background

Currently there is no simple, canonical URL on the wiki that directs mobile users to the correct app store and we want to avoid using 3rd party tracking. Having a single Special:MobileAppRedirect page would allow us to use one link in banners, emails, QR codes, etc., and have users automatically land in the right store.

Proposed Behavior

  • Server-side detection via the User-Agent header
  • If iOS is detected (iPhone, iPad, iPod in UA string), redirect to the App Store URL
  • If Android is detected (Android in UA string), redirect to the Google Play Store URL
  • If neither is detected, render a fallback page with links to both stores
  • URLs configurable via wiki config variables rather than hardcoded?
  • A campaign URL parameter should be accepted and passed through to the store URLs
    • Example: Special:MobileAppRedirect?campaign=footer_banner_2026
    • For Google Play, appended as &referrer=utm_campaign%3D{campaign}
    • For App Store, appended as &ct={campaign}
    • If no campaign parameter is provided, redirect without campaign tracking

Acceptance Criteria

  • Visiting Special:MobileAppRedirect on an iPhone/iPad redirects to the App Store
  • Visiting Special:MobileAppRedirect on an Android device redirects to the Google Play Store
  • Visiting on a desktop browser shows a page with links to both stores
  • campaign parameter is passed through to the appropriate store-specific tracking parameter
  • Campaign value is properly sanitized/URL-encoded
  • Redirect works correctly with and without a campaign parameter
  • Special page is registered and appears in Special:SpecialPages
  • Verify we handle iPadOS correctly (Recent iPadOS versions can report a desktop Safari UA.)

Event Timeline

Claude's musings on this matter:

includes/specials/SpecialMobileAppRedirect.php

<?php

use MediaWiki\MediaWikiServices;

class SpecialMobileAppRedirect extends SpecialPage {

private const INSTRUMENT_NAME = 'mobile-app-redirect';

public function __construct() {
    parent::__construct( 'MobileAppRedirect' );
}

public function execute( $subPage ) {
    $out = $this->getOutput();
    $config = $this->getConfig();
    $request = $this->getRequest();

    $ua = $request->getHeader( 'User-Agent' ) ?: '';
    $campaign = $request->getVal( 'campaign' );

    $iosUrl = $this->buildIOSUrl(
        $config->get( 'MobileAppAppleStoreUrl' ),
        $campaign
    );
    $androidUrl = $this->buildAndroidUrl(
        $config->get( 'MobileAppGooglePlayUrl' ),
        $campaign
    );

    if ( $this->isIOS( $ua ) ) {
        $this->logVisit( 'ios', $campaign, $iosUrl );
        $out->redirect( $iosUrl );
        return;
    }

    if ( $this->isAndroid( $ua ) ) {
        $this->logVisit( 'android', $campaign, $androidUrl );
        $out->redirect( $androidUrl );
        return;
    }

    // Fallback: show both links
    $this->logVisit( 'other', $campaign, null );
    $this->setHeaders();
    $out->addHTML(
        Html::element( 'a', [ 'href' => $iosUrl ], 'Download on the App Store' )
        . Html::element( 'br' )
        . Html::element( 'a', [ 'href' => $androidUrl ], 'Get it on Google Play' )
    );
}

/**
 * Log a visit event to product_metrics.web_base stream
 * via the Test Kitchen PHP SDK.
 *
 * @param string $platform 'ios', 'android', or 'other'
 * @param string|null $campaign Campaign ID from URL parameter
 * @param string|null $redirectUrl The store URL being redirected to, or null for fallback
 */
private function logVisit(
    string $platform,
    ?string $campaign,
    ?string $redirectUrl
): void {
    $action = $redirectUrl ? 'redirect' : 'fallback';

    $interactionData = [
        'action_source' => 'MobileAppRedirect',
        'action_context' => json_encode( [
            'platform' => $platform,
            'campaign' => $campaign ?? '',
            'redirect_url' => $redirectUrl ?? '',
        ] ),
    ];

    $instrumentManager = MediaWikiServices::getInstance()
        ->getService( 'TestKitchen.Sdk.InstrumentManager' );
    $instrument = $instrumentManager->getInstrument( self::INSTRUMENT_NAME );
    $instrument->send( $action, $interactionData );
}

/**
 * @param string $ua User-Agent header value
 * @return bool
 */
private function isIOS( string $ua ): bool {
    if ( preg_match( '/iPhone|iPad|iPod/i', $ua ) ) {
        return true;
    }

    // iPadOS 13+ reports as desktop Safari on macOS.
    // Match Macintosh + Safari without Chrome/Firefox as a heuristic.
    // Trade-off: real desktop Safari users will get the iOS redirect.
    if ( preg_match( '/Macintosh.*Mac OS X/i', $ua )
        && preg_match( '/Safari/i', $ua )
        && !preg_match( '/Chrome|Chromium|Firefox/i', $ua )
    ) {
        return true;
    }

    return false;
}

/**
 * @param string $ua User-Agent header value
 * @return bool
 */
private function isAndroid( string $ua ): bool {
    return (bool)preg_match( '/Android/i', $ua );
}

/**
 * @param string $baseUrl App Store base URL
 * @param string|null $campaign Campaign ID
 * @return string
 */
private function buildIOSUrl( string $baseUrl, ?string $campaign ): string {
    if ( $campaign !== null && $campaign !== '' ) {
        $baseUrl = wfAppendQuery( $baseUrl, [
            'ct' => $campaign,
        ] );
    }
    return $baseUrl;
}

/**
 * @param string $baseUrl Google Play base URL
 * @param string|null $campaign Campaign ID
 * @return string
 */
private function buildAndroidUrl( string $baseUrl, ?string $campaign ): string {
    if ( $campaign !== null && $campaign !== '' ) {
        $baseUrl = wfAppendQuery( $baseUrl, [
            'referrer' => 'utm_campaign=' . urlencode( $campaign ),
        ] );
    }
    return $baseUrl;
}

}
And the corresponding extension.json additions for reference:
json{

"SpecialPages": {
    "MobileAppRedirect": "SpecialMobileAppRedirect"
},
"AutoloadClasses": {
    "SpecialMobileAppRedirect": "includes/specials/SpecialMobileAppRedirect.php"
},
"config": {
    "MobileAppAppleStoreUrl": {
        "value": "https://apps.apple.com/app/your-app-id",
        "description": "URL for the iOS App Store listing"
    },
    "MobileAppGooglePlayUrl": {
        "value": "https://play.google.com/store/apps/details?id=your.package.name",
        "description": "URL for the Google Play Store listing"
    }
}

}
And i18n/en.json:
json{

"mobileappredirect": "Mobile App Redirect"

}

Just chiming in that this would be a very nice to have feature for the Attribution Framework + API. One of the calls to action that we have is recommending that people download the app, so it would be very helpful to have the logic sorted out and/or a page to link to. Do y'all have a sense of when this might be made available?

Assuming that SpecialMobileAppRedirect doesn't exist yet, I'd recommend placing it in WikimediaCustomizations

Dbrant set the point value for this task to 8.May 4 2026, 5:31 PM

We chatted about this within the MediaWiki team and @pmiazga took a look at the speculative code. I think we are good from our side. I just want to confirm our assumptions to make sure we're all on the same page:

  • The Attribution API would blindly reference this page/link directly. We will not add any additional logic for device detection, language, etc.
  • If the page is accessed from a mobile device, the user will be automatically redirected to the appropriate app store.
  • If the page is accessed from a desktop browser, it will be a basic "Download the app" interface.

I have a few additional questions as well:

  1. Is it reasonable to assume that this would exist on every Wikimedia project, and that we should fetch it from the source of the article or image? I assume that would help with language redirects and like?
  2. Do you expect any issues from the redirect itself? Like, is there a risk that browsers will block it or throw a warning or anything since it's a special page directing to an external site? I assume not, but wanted to double check.
  3. What does 'campaign' mean in this case? Are you expecting that we would have a unique campaign for the Attribution API, to know that's where they came in from generally? Or is this a value you would expect to get passed in from somewhere else, where it's perhaps unique per client or something?

Just curious if this is going anywhere. Do y'all have a timeline for if/when you might add this page?

Hey -- just checking in again. Any idea of when this might be available? For more context, we have another hypothesis this quarter for the Attribution API, which includes improving our calls to action. One of the main calls to action we would like to include is basically "download the app" so getting this up and running would be ideal. We can figure out a temporary workaround that improves on the current experience, but knowing if/when you might pull this in would be helpful to know.

Hey -- just checking in again. Any idea of when this might be available? For more context, we have another hypothesis this quarter for the Attribution API, which includes improving our calls to action. One of the main calls to action we would like to include is basically "download the app" so getting this up and running would be ideal. We can figure out a temporary workaround that improves on the current experience, but knowing if/when you might pull this in would be helpful to know.

hiya per my comment on T423737#12075442 if it looks like this may be a while, I'd suggest a temp URL like https://wikimediafoundation.org/news/2026/05/19/want-to-read-more-download-the-wikipedia-app/ as it has the benefit of having more prominent download links and providing information about the app (vs the portal page) and more mass-audience friendly than mediawiki pages.

Change #1314863 had a related patch set uploaded (by Seddon; author: Seddon):

[mediawiki/extensions/MobileApp@master] MobileApp: Add Special:MobileAppRedirect for app store redirection

https://gerrit.wikimedia.org/r/1314863