User updating their profile wipes my custom fields

I see two problems in your code. The first one is the user capability level and the second one is how you’re saving the data.

You can fix the first one by setting the capability requirement in your saving function higher, some capability that only administrators have. Regarding edit_usercapability, WordPress Capabilities: edit_user vs edit_users

The second one you can fix by adding isset() check to see, if the custom field values are being sent or not with $_POST. At the current form, if the custom field value is not set, then $_POST['field-key] results in null value, which overwrites any previous saved data. I.e. update_user_meta( $user_id, 'pin', $_POST['pin'] );

Here’s one example how you could improve your code. In my example I’ve
added a nonce check, upgraded the capability requirement, added isset() check and data sanitization.

function extra_user_profile_fields( $user ) {
  // add nonce field
  wp_nonce_field( 'my_extra_user_profile_fields', 'my_extra_user_profile_fields_nonce', true, true );
  ?>
  <!-- your html as it was -->
  <?php
}

function save_extra_user_profile_fields( $user_id ) {
  // Nonce checks to know the $_POST is coming from the right source
  if ( empty( $_POST['my_extra_user_profile_fields_nonce'] ) || ! wp_verify_nonce( 'my_extra_user_profile_fields_nonce', 'my_extra_user_profile_fields' ) ) {
    return;
  }
  // Capabilities check
  // create_users is administrator only capability
  if ( ! current_user_can( 'create_users', $user_id ) ) {
    return;
  }
  // valid field keys with related data type
  // update types as needed
  $fields = array(
    'pin'                        => 'string',
    'street-address'             => 'string',
    'suburb'                     => 'string',
    'postcode'                   => 'string',
    'phone'                      => 'string',
    'mobile'                     => 'string',
    'president-year'             => 'int',
    'secretary-year'             => 'int',
    'competition-secretary-year' => 'int',
    'treasurer-year'             => 'int',
    'committee-member-year'      => 'int',
    'joining-date'               => 'string',
    'fees-paid'                  => 'bool',
    'life-member'                => 'string',
    'comments'                   => 'string',
    'other-roles'                => 'string',
    'year-left'                  => 'int',
  );
  // loop fields instead of typing each update separately
  foreach ($fields as $key => $data_type) {
    // check if field data is sent with $_POST
    if ( isset( $_POST[$key] ) ) {
      // update user meta with sanitized value
      update_user_meta( $user_id, $key, my_sanitize_user_data($_POST[$key], $data_type) );
    }
  } 
}

function my_sanitize_user_data( $value, string $type="" ) {
  switch ($type) {
    case 'bool':
      return in_array( $value, array(true, 'true', 1, 'yes') ); // returns true, if value is in array otherwise false
    case 'int':
      return is_numeric($value) ? absint( $value ) : 0;
    default:
      return sanitize_text_field( $value );
  }
}

I think you should also be able to remove add_action( 'personal_options_update', 'save_extra_user_profile_fields' ); as add_action( 'edit_user_profile_update', 'save_extra_user_profile_fields' ); covers the data saving.